shinestacker 1.9.0__tar.gz → 1.9.1__tar.gz

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 shinestacker might be problematic. Click here for more details.

Files changed (171) hide show
  1. {shinestacker-1.9.0 → shinestacker-1.9.1}/CHANGELOG.md +18 -1
  2. {shinestacker-1.9.0/src/shinestacker.egg-info → shinestacker-1.9.1}/PKG-INFO +1 -1
  3. {shinestacker-1.9.0 → shinestacker-1.9.1}/pyproject.toml +2 -0
  4. shinestacker-1.9.1/src/shinestacker/_version.py +1 -0
  5. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/exif.py +133 -73
  6. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/stack.py +1 -1
  7. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/app/main.py +1 -1
  8. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/action_config_dialog.py +0 -4
  9. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/config_dialog.py +6 -5
  10. shinestacker-1.9.1/src/shinestacker/retouch/exif_data.py +75 -0
  11. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/image_editor_ui.py +45 -2
  12. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/io_gui_handler.py +9 -11
  13. {shinestacker-1.9.0 → shinestacker-1.9.1/src/shinestacker.egg-info}/PKG-INFO +1 -1
  14. shinestacker-1.9.0/src/shinestacker/_version.py +0 -1
  15. shinestacker-1.9.0/src/shinestacker/retouch/exif_data.py +0 -55
  16. {shinestacker-1.9.0 → shinestacker-1.9.1}/.coveragerc +0 -0
  17. {shinestacker-1.9.0 → shinestacker-1.9.1}/.flake8 +0 -0
  18. {shinestacker-1.9.0 → shinestacker-1.9.1}/.github/workflows/ci-multiplatform.yml +0 -0
  19. {shinestacker-1.9.0 → shinestacker-1.9.1}/.github/workflows/pylint.yml +0 -0
  20. {shinestacker-1.9.0 → shinestacker-1.9.1}/.github/workflows/pypi-publish.yml +0 -0
  21. {shinestacker-1.9.0 → shinestacker-1.9.1}/.github/workflows/release.yml +0 -0
  22. {shinestacker-1.9.0 → shinestacker-1.9.1}/.gitignore +0 -0
  23. {shinestacker-1.9.0 → shinestacker-1.9.1}/.pylintrc +0 -0
  24. {shinestacker-1.9.0 → shinestacker-1.9.1}/.readthedocs.yaml +0 -0
  25. {shinestacker-1.9.0 → shinestacker-1.9.1}/LICENSE +0 -0
  26. {shinestacker-1.9.0 → shinestacker-1.9.1}/MANIFEST.in +0 -0
  27. {shinestacker-1.9.0 → shinestacker-1.9.1}/README.md +0 -0
  28. {shinestacker-1.9.0 → shinestacker-1.9.1}/THIRD_PARTY_LICENSES.txt +0 -0
  29. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/alignment.md +0 -0
  30. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/api.md +0 -0
  31. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/balancing.md +0 -0
  32. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/conf.py +0 -0
  33. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/focus_stacking.md +0 -0
  34. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/gui.md +0 -0
  35. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/index.md +0 -0
  36. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/job.md +0 -0
  37. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/macos-install.md +0 -0
  38. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/main.md +0 -0
  39. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/multilayer.md +0 -0
  40. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/noise.md +0 -0
  41. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/requirements.txt +0 -0
  42. {shinestacker-1.9.0 → shinestacker-1.9.1}/docs/vignetting.md +0 -0
  43. {shinestacker-1.9.0 → shinestacker-1.9.1}/img/coffee.gif +0 -0
  44. {shinestacker-1.9.0 → shinestacker-1.9.1}/img/coffee_stack.jpg +0 -0
  45. {shinestacker-1.9.0 → shinestacker-1.9.1}/img/extreme-vignetting.jpg +0 -0
  46. {shinestacker-1.9.0 → shinestacker-1.9.1}/img/flies.gif +0 -0
  47. {shinestacker-1.9.0 → shinestacker-1.9.1}/img/flies_stack.jpg +0 -0
  48. {shinestacker-1.9.0 → shinestacker-1.9.1}/img/flow-diagram.png +0 -0
  49. {shinestacker-1.9.0 → shinestacker-1.9.1}/img/gui-project-new.png +0 -0
  50. {shinestacker-1.9.0 → shinestacker-1.9.1}/img/gui-project-run.png +0 -0
  51. {shinestacker-1.9.0 → shinestacker-1.9.1}/img/gui-retouch.png +0 -0
  52. {shinestacker-1.9.0 → shinestacker-1.9.1}/index.html +0 -0
  53. {shinestacker-1.9.0 → shinestacker-1.9.1}/requirements.txt +0 -0
  54. {shinestacker-1.9.0 → shinestacker-1.9.1}/scripts/build_release.py +0 -0
  55. {shinestacker-1.9.0 → shinestacker-1.9.1}/scripts/create_macos_icon.py +0 -0
  56. {shinestacker-1.9.0 → shinestacker-1.9.1}/scripts/git-rev-list.sh +0 -0
  57. {shinestacker-1.9.0 → shinestacker-1.9.1}/scripts/hooks/hook-IPython.py +0 -0
  58. {shinestacker-1.9.0 → shinestacker-1.9.1}/scripts/hooks/hook-PySide6.py +0 -0
  59. {shinestacker-1.9.0 → shinestacker-1.9.1}/scripts/hooks/hook-opencv.py +0 -0
  60. {shinestacker-1.9.0 → shinestacker-1.9.1}/scripts/hooks/hook-tests.py +0 -0
  61. {shinestacker-1.9.0 → shinestacker-1.9.1}/scripts/scan_imports.py +0 -0
  62. {shinestacker-1.9.0 → shinestacker-1.9.1}/scripts/shinestacker-inno-setup.iss +0 -0
  63. {shinestacker-1.9.0 → shinestacker-1.9.1}/scripts/validate-tomli.py +0 -0
  64. {shinestacker-1.9.0 → shinestacker-1.9.1}/setup.cfg +0 -0
  65. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/__init__.py +0 -0
  66. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/__init__.py +0 -0
  67. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/align.py +0 -0
  68. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/align_auto.py +0 -0
  69. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/align_parallel.py +0 -0
  70. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/balance.py +0 -0
  71. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  72. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/corrections.py +0 -0
  73. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/denoise.py +0 -0
  74. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/depth_map.py +0 -0
  75. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/multilayer.py +0 -0
  76. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/noise_detection.py +0 -0
  77. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/pyramid.py +0 -0
  78. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/pyramid_auto.py +0 -0
  79. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/pyramid_tiles.py +0 -0
  80. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/sharpen.py +0 -0
  81. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/stack_framework.py +0 -0
  82. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/utils.py +0 -0
  83. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/vignetting.py +0 -0
  84. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/algorithms/white_balance.py +0 -0
  85. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/app/__init__.py +0 -0
  86. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/app/about_dialog.py +0 -0
  87. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/app/args_parser_opts.py +0 -0
  88. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/app/gui_utils.py +0 -0
  89. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/app/help_menu.py +0 -0
  90. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/app/open_frames.py +0 -0
  91. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/app/project.py +0 -0
  92. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/app/retouch.py +0 -0
  93. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/app/settings_dialog.py +0 -0
  94. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/config/__init__.py +0 -0
  95. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/config/app_config.py +0 -0
  96. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/config/config.py +0 -0
  97. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/config/constants.py +0 -0
  98. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/config/gui_constants.py +0 -0
  99. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/config/settings.py +0 -0
  100. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/core/__init__.py +0 -0
  101. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/core/colors.py +0 -0
  102. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/core/core_utils.py +0 -0
  103. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/core/exceptions.py +0 -0
  104. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/core/framework.py +0 -0
  105. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/core/logging.py +0 -0
  106. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/__init__.py +0 -0
  107. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/action_config.py +0 -0
  108. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/base_form_dialog.py +0 -0
  109. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/colors.py +0 -0
  110. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/flow_layout.py +0 -0
  111. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/folder_file_selection.py +0 -0
  112. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/gui_images.py +0 -0
  113. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/gui_logging.py +0 -0
  114. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/gui_run.py +0 -0
  115. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  116. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  117. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  118. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  119. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/img/dark/close-round-line-icon.png +0 -0
  120. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/img/dark/forward-button-icon.png +0 -0
  121. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/img/dark/play-button-round-icon.png +0 -0
  122. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/img/dark/plus-round-line-icon.png +0 -0
  123. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/img/dark/shinestacker_bkg.png +0 -0
  124. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/img/light/close-round-line-icon.png +0 -0
  125. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/img/light/forward-button-icon.png +0 -0
  126. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/img/light/play-button-round-icon.png +0 -0
  127. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/img/light/plus-round-line-icon.png +0 -0
  128. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/img/light/shinestacker_bkg.png +0 -0
  129. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/main_window.py +0 -0
  130. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/menu_manager.py +0 -0
  131. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/new_project.py +0 -0
  132. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/project_controller.py +0 -0
  133. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/project_converter.py +0 -0
  134. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/project_editor.py +0 -0
  135. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/project_model.py +0 -0
  136. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/recent_file_manager.py +0 -0
  137. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/select_path_widget.py +0 -0
  138. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/sys_mon.py +0 -0
  139. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/tab_widget.py +0 -0
  140. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/gui/time_progress_bar.py +0 -0
  141. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/__init__.py +0 -0
  142. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/adjustments.py +0 -0
  143. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/base_filter.py +0 -0
  144. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/brush.py +0 -0
  145. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/brush_gradient.py +0 -0
  146. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/brush_preview.py +0 -0
  147. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/brush_tool.py +0 -0
  148. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/denoise_filter.py +0 -0
  149. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/display_manager.py +0 -0
  150. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/file_loader.py +0 -0
  151. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/filter_manager.py +0 -0
  152. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/icon_container.py +0 -0
  153. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/image_view_status.py +0 -0
  154. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/image_viewer.py +0 -0
  155. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/io_threads.py +0 -0
  156. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/layer_collection.py +0 -0
  157. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/overlaid_view.py +0 -0
  158. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/paint_area_manager.py +0 -0
  159. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  160. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/sidebyside_view.py +0 -0
  161. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/transformation_manager.py +0 -0
  162. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/undo_manager.py +0 -0
  163. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  164. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/view_strategy.py +0 -0
  165. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  166. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  167. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker.egg-info/SOURCES.txt +0 -0
  168. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  169. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker.egg-info/entry_points.txt +0 -0
  170. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker.egg-info/requires.txt +0 -0
  171. {shinestacker-1.9.0 → shinestacker-1.9.1}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,9 +2,26 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+ ## [v1.9.1] - 2025-10-23
6
+ ** Improved EXIF data handling GUI refinements **
7
+
8
+ ### Added
9
+ - missing actions "Show EXIF Data" and "Delete EXIF Data"
10
+
11
+ ### Fixed
12
+ - if saving EXIF data fails, a warning is issued instead of stopping the run
13
+ - exif data correctly loaded when opening image file in retouch mode
14
+ - removed duplicated parameter from config dialog (FocusStackBunch)
15
+ - fixed thread warning when the application quits
16
+ - more robust EXIF data code for JPG format prevents possible errors
17
+
18
+ ### Changed
19
+ - improved EXIF GUI: more data displayed and improved selection logic
20
+
21
+ -----
5
22
 
6
23
  ## [v1.9.0] - 2025-10-19
7
- ** Added PNG format support and EXIF failure fix**
24
+ ** Added PNG format support and fixed EXIF failure **
8
25
 
9
26
  ### Added
10
27
  - support of images in PNG format, both in 8 bit and 16 bit depth. Note: EXIF data are not supported for 16 bit PNG because of limitations in the PIL and Open CV python libraries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.9.0
3
+ Version: 1.9.1
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -64,6 +64,8 @@ packages = [
64
64
  "shinestacker.algorithms",
65
65
  "shinestacker.gui",
66
66
  "shinestacker.gui.img",
67
+ "shinestacker.gui.img.dark",
68
+ "shinestacker.gui.img.light",
67
69
  "shinestacker.retouch"
68
70
  ]
69
71
  include-package-data = true
@@ -0,0 +1 @@
1
+ __version__ = '1.9.1'
@@ -1,7 +1,5 @@
1
1
  # pylint: disable=C0114, C0116, W0718, R0911, R0912, E1101, R0915, R1702, R0914, R0917, R0913
2
2
  import os
3
- import re
4
- import io
5
3
  import logging
6
4
  import traceback
7
5
  import cv2
@@ -37,14 +35,17 @@ NO_COPY_TIFF_TAGS = ["Compression", "StripOffsets", "RowsPerStrip", "StripByteCo
37
35
 
38
36
 
39
37
  def extract_enclosed_data_for_jpg(data, head, foot):
40
- size = len(foot.decode('ascii'))
41
- xmp_start, xmp_end = data.find(head), data.find(foot)
42
- if xmp_start != -1 and xmp_end != -1:
43
- return re.sub(
44
- b'[^\x20-\x7E]', b'',
45
- data[xmp_start:xmp_end + size]
46
- ).decode().replace('\x00', '').encode()
47
- return None
38
+ try:
39
+ xmp_start = data.find(head)
40
+ if xmp_start == -1:
41
+ return None
42
+ xmp_end = data.find(foot, xmp_start)
43
+ if xmp_end == -1:
44
+ return None
45
+ xmp_end += len(foot)
46
+ return data[xmp_start:xmp_end]
47
+ except Exception:
48
+ return None
48
49
 
49
50
 
50
51
  def get_exif(exif_filename):
@@ -87,6 +88,21 @@ def get_exif_from_png(image):
87
88
  return exif_data
88
89
 
89
90
 
91
+ def safe_decode_bytes(data, encoding='utf-8'):
92
+ if not isinstance(data, bytes):
93
+ return data
94
+ encodings = [encoding, 'latin-1', 'cp1252', 'utf-16', 'ascii']
95
+ for enc in encodings:
96
+ try:
97
+ return data.decode(enc, errors='strict')
98
+ except UnicodeDecodeError:
99
+ continue
100
+ try:
101
+ return data.decode('utf-8', errors='replace')
102
+ except Exception:
103
+ return "<<< decode error >>>"
104
+
105
+
90
106
  def exif_extra_tags_for_tif(exif):
91
107
  logger = logging.getLogger(__name__)
92
108
  res_x, res_y = exif.get(RESOLUTIONX), exif.get(RESOLUTIONY)
@@ -107,8 +123,13 @@ def exif_extra_tags_for_tif(exif):
107
123
  try:
108
124
  if tag_id not in (IMAGERESOURCES, INTERCOLORPROFILE):
109
125
  if tag_id == XMLPACKET:
110
- data = re.sub(b'[^\x20-\x7E]', b'', data)
111
- data = data.decode()
126
+ try:
127
+ decoded = data.decode('utf-8')
128
+ data = decoded.encode('utf-8')
129
+ except UnicodeDecodeError:
130
+ logger.debug("XMLPACKET contains non-UTF8 data, preserving as bytes")
131
+ else:
132
+ data = safe_decode_bytes(data)
112
133
  except Exception:
113
134
  logger.warning(msg=f"Copy: can't decode EXIF tag {tag:25} [#{tag_id}]")
114
135
  data = '<<< decode error >>>'
@@ -151,32 +172,71 @@ def get_tiff_dtype_count(value):
151
172
  return 2, len(str(value)) + 1 # Default for othre cases (ASCII string)
152
173
 
153
174
 
154
- def add_exif_data_to_jpg_file(exif, in_filenama, out_filename, verbose=False):
175
+ def add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose=False):
155
176
  logger = logging.getLogger(__name__)
156
177
  if exif is None:
157
178
  raise RuntimeError('No exif data provided.')
158
179
  if verbose:
159
180
  print_exif(exif)
160
- xmp_data = extract_enclosed_data_for_jpg(exif[XMLPACKET], b'<x:xmpmeta', b'</x:xmpmeta>')
161
- with Image.open(in_filenama) as image:
162
- with io.BytesIO() as buffer:
163
- image.save(buffer, format="JPEG", exif=exif.tobytes(), quality=100)
164
- jpeg_data = buffer.getvalue()
165
- if xmp_data is not None:
166
- app1_marker_pos = jpeg_data.find(b'\xFF\xE1')
167
- if app1_marker_pos == -1:
168
- app1_marker_pos = len(jpeg_data) - 2
169
- updated_data = (
170
- jpeg_data[:app1_marker_pos] +
171
- b'\xFF\xE1' + len(xmp_data).to_bytes(2, 'big') +
172
- xmp_data + jpeg_data[app1_marker_pos:]
173
- )
174
- else:
175
- logger.warning("Copy: can't find XMLPacket in JPG EXIF data")
176
- updated_data = jpeg_data
177
- with open(out_filename, 'wb') as f:
178
- f.write(updated_data)
179
- return exif
181
+ xmp_data = None
182
+ if XMLPACKET in exif:
183
+ xmp_data = exif[XMLPACKET]
184
+ if isinstance(xmp_data, bytes):
185
+ xmp_start = xmp_data.find(b'<x:xmpmeta')
186
+ xmp_end = xmp_data.find(b'</x:xmpmeta>')
187
+ if xmp_start != -1 and xmp_end != -1:
188
+ xmp_end += len(b'</x:xmpmeta>')
189
+ xmp_data = xmp_data[xmp_start:xmp_end]
190
+ with Image.open(in_filename) as image:
191
+ if hasattr(exif, 'tobytes'):
192
+ exif_bytes = exif.tobytes()
193
+ else:
194
+ exif_bytes = exif
195
+ image.save(out_filename, "JPEG", exif=exif_bytes, quality=100)
196
+ if xmp_data and isinstance(xmp_data, bytes):
197
+ try:
198
+ _insert_xmp_into_jpeg(out_filename, xmp_data, verbose)
199
+ except Exception as e:
200
+ if verbose:
201
+ logger.warning(msg=f"Failed to insert XMP data: {e}")
202
+
203
+
204
+ def _insert_xmp_into_jpeg(jpeg_path, xmp_data, verbose=False):
205
+ logger = logging.getLogger(__name__)
206
+ with open(jpeg_path, 'rb') as f:
207
+ jpeg_data = f.read()
208
+ soi_pos = jpeg_data.find(b'\xFF\xD8')
209
+ if soi_pos == -1:
210
+ if verbose:
211
+ logger.warning("No SOI marker found, cannot insert XMP")
212
+ return
213
+ insert_pos = soi_pos + 2
214
+ current_pos = insert_pos
215
+ while current_pos < len(jpeg_data) - 4:
216
+ if jpeg_data[current_pos] != 0xFF:
217
+ break
218
+ marker = jpeg_data[current_pos + 1]
219
+ if marker == 0xDA:
220
+ break
221
+ segment_length = int.from_bytes(jpeg_data[current_pos + 2:current_pos + 4], 'big')
222
+ if marker == 0xE1:
223
+ insert_pos = current_pos + 2 + segment_length
224
+ current_pos = insert_pos
225
+ continue
226
+ current_pos += 2 + segment_length
227
+ xmp_identifier = b'http://ns.adobe.com/xap/1.0/\x00'
228
+ xmp_payload = xmp_identifier + xmp_data
229
+ segment_length = len(xmp_payload) + 2
230
+ xmp_segment = b'\xFF\xE1' + segment_length.to_bytes(2, 'big') + xmp_payload
231
+ updated_data = (
232
+ jpeg_data[:insert_pos] +
233
+ xmp_segment +
234
+ jpeg_data[insert_pos:]
235
+ )
236
+ with open(jpeg_path, 'wb') as f:
237
+ f.write(updated_data)
238
+ if verbose:
239
+ logger.info("Successfully inserted XMP data into JPEG")
180
240
 
181
241
 
182
242
  def create_xmp_from_exif(exif_data):
@@ -263,7 +323,23 @@ def write_image_with_exif_data_png(exif, image, out_filename, verbose=False, col
263
323
  return
264
324
  pil_image = _convert_to_pil_image(image, color_order, verbose, logger)
265
325
  pnginfo, icc_profile = _prepare_png_metadata(exif, verbose, logger)
266
- _save_png_with_metadata(pil_image, out_filename, pnginfo, icc_profile, verbose, logger)
326
+ try:
327
+ save_args = {'format': 'PNG', 'pnginfo': pnginfo}
328
+ if icc_profile:
329
+ save_args['icc_profile'] = icc_profile
330
+ if verbose:
331
+ logger.info(msg="Saved PNG with ICC profile and metadata")
332
+ else:
333
+ if verbose:
334
+ logger.info(msg="Saved PNG without ICC profile but with metadata")
335
+ pil_image.save(out_filename, **save_args)
336
+ if verbose:
337
+ logger.info(msg=f"Successfully wrote PNG with metadata: {out_filename}")
338
+ except Exception as e:
339
+ if verbose:
340
+ logger.error(msg=f"Failed to write PNG with metadata: {e}")
341
+ logger.error(traceback.format_exc())
342
+ pil_image.save(out_filename, format='PNG')
267
343
 
268
344
 
269
345
  def _convert_to_pil_image(image, color_order, verbose, logger):
@@ -374,24 +450,17 @@ def _extract_icc_profile(exif, verbose, logger):
374
450
  return None
375
451
 
376
452
 
377
- def _save_png_with_metadata(pil_image, out_filename, pnginfo, icc_profile, verbose, logger):
378
- try:
379
- save_args = {'format': 'PNG', 'pnginfo': pnginfo}
380
- if icc_profile:
381
- save_args['icc_profile'] = icc_profile
382
- if verbose:
383
- logger.info(msg="Saved PNG with ICC profile and metadata")
384
- else:
385
- if verbose:
386
- logger.info(msg="Saved PNG without ICC profile but with metadata")
387
- pil_image.save(out_filename, **save_args)
388
- if verbose:
389
- logger.info(msg=f"Successfully wrote PNG with metadata: {out_filename}")
390
- except Exception as e:
391
- if verbose:
392
- logger.error(msg=f"Failed to write PNG with metadata: {e}")
393
- logger.error(traceback.format_exc())
394
- pil_image.save(out_filename, format='PNG')
453
+ def write_image_with_exif_data_jpg(exif, image, out_filename, verbose):
454
+ cv2.imwrite(out_filename, image, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
455
+ add_exif_data_to_jpg_file(exif, out_filename, out_filename, verbose)
456
+
457
+
458
+ def write_image_with_exif_data_tif(exif, image, out_filename):
459
+ metadata = {"description": f"image generated with {constants.APP_STRING} package"}
460
+ extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
461
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
462
+ tifffile.imwrite(out_filename, image, metadata=metadata, compression='adobe_deflate',
463
+ extratags=extra_tags, **exif_tags)
395
464
 
396
465
 
397
466
  def write_image_with_exif_data(exif, image, out_filename, verbose=False, color_order='auto'):
@@ -401,13 +470,9 @@ def write_image_with_exif_data(exif, image, out_filename, verbose=False, color_o
401
470
  if verbose:
402
471
  print_exif(exif)
403
472
  if extension_jpg(out_filename):
404
- cv2.imwrite(out_filename, image, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
405
- add_exif_data_to_jpg_file(exif, out_filename, out_filename, verbose)
473
+ write_image_with_exif_data_jpg(exif, image, out_filename, verbose)
406
474
  elif extension_tif(out_filename):
407
- metadata = {"description": f"image generated with {constants.APP_STRING} package"}
408
- extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
409
- tifffile.imwrite(out_filename, image, metadata=metadata, compression='adobe_deflate',
410
- extratags=extra_tags, **exif_tags)
475
+ write_image_with_exif_data_tif(exif, image, out_filename)
411
476
  elif extension_png(out_filename):
412
477
  write_image_with_exif_data_png(exif, image, out_filename, verbose, color_order=color_order)
413
478
  return exif
@@ -447,22 +512,13 @@ def copy_exif_from_file_to_file(exif_filename, in_filename, out_filename=None, v
447
512
  return save_exif_data(exif, in_filename, out_filename, verbose)
448
513
 
449
514
 
450
- def exif_dict(exif, hide_xml=True):
515
+ def exif_dict(exif):
451
516
  if exif is None:
452
517
  return None
453
518
  exif_data = {}
454
519
  for tag_id in exif:
455
520
  tag = TAGS.get(tag_id, tag_id)
456
- if tag_id == XMLPACKET and hide_xml:
457
- data = "<<< XML data >>>"
458
- elif tag_id in (IMAGERESOURCES, INTERCOLORPROFILE):
459
- data = "<<< Photoshop data >>>"
460
- elif tag_id == STRIPOFFSETS:
461
- data = "<<< Strip offsets >>>"
462
- elif tag_id == STRIPBYTECOUNTS:
463
- data = "<<< Strip byte counts >>>"
464
- else:
465
- data = exif.get(tag_id) if hasattr(exif, 'get') else exif[tag_id]
521
+ data = exif.get(tag_id) if hasattr(exif, 'get') else exif[tag_id]
466
522
  if isinstance(data, bytes):
467
523
  try:
468
524
  data = data.decode()
@@ -472,15 +528,19 @@ def exif_dict(exif, hide_xml=True):
472
528
  return exif_data
473
529
 
474
530
 
475
- def print_exif(exif, hide_xml=True):
476
- exif_data = exif_dict(exif, hide_xml)
531
+ def print_exif(exif):
532
+ exif_data = exif_dict(exif)
477
533
  if exif_data is None:
478
534
  raise RuntimeError('Image has no exif data.')
479
535
  logger = logging.getLogger(__name__)
480
536
  for tag, (tag_id, data) in exif_data.items():
481
537
  if isinstance(data, IFDRational):
482
538
  data = f"{data.numerator}/{data.denominator}"
539
+ data_str = f"{data}"
540
+ if len(data_str) > 40:
541
+ data_str = f"{data_str[:40]}..."
483
542
  if isinstance(tag_id, int):
484
- logger.info(msg=f"{tag:25} [#{tag_id:5d}]: {data}")
543
+ tag_id_str = f"[#{tag_id:5d}]"
485
544
  else:
486
- logger.info(msg=f"{tag:25} [ {tag_id:20} ]: {str(data)[:100]}...")
545
+ tag_id_str = f"[ {tag_id:20} ]"
546
+ logger.info(msg=f"{tag:25} {tag_id_str}: {data_str}")
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, R0913, R0917
1
+ # pylint: disable=C0114, C0115, C0116, R0913, R0917, W0718
2
2
  import os
3
3
  import traceback
4
4
  import logging
@@ -199,7 +199,7 @@ class MainApp(QMainWindow):
199
199
  class Application(QApplication):
200
200
  def event(self, event):
201
201
  if event.type() == QEvent.Quit and event.spontaneous():
202
- if not self.quit():
202
+ if not self.main_app.quit():
203
203
  return True
204
204
  return super().event(event)
205
205
 
@@ -372,10 +372,6 @@ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
372
372
  self.add_field_to_layout(
373
373
  self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
374
374
  default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
375
- self.add_field_to_layout(
376
- self.general_tab_layout, 'scratch_output_dir', FIELD_BOOL,
377
- 'Scratch output folder before run',
378
- required=False, default=True)
379
375
  self.add_field_to_layout(
380
376
  self.general_tab_layout, 'delete_output_at_end', FIELD_BOOL,
381
377
  'Delete output at end of job',
@@ -12,6 +12,7 @@ class ConfigDialog(QDialog):
12
12
  self.form_layout = create_form_layout(self)
13
13
  scroll_area = QScrollArea()
14
14
  scroll_area.setWidgetResizable(True)
15
+ scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
15
16
  container_widget = QWidget()
16
17
  self.container_layout = QFormLayout(container_widget)
17
18
  self.container_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
@@ -19,19 +20,19 @@ class ConfigDialog(QDialog):
19
20
  self.container_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
20
21
  self.container_layout.setLabelAlignment(Qt.AlignLeft)
21
22
  scroll_area.setWidget(container_widget)
22
- button_box = QHBoxLayout()
23
+ self.button_box = QHBoxLayout()
23
24
  self.ok_button = QPushButton("OK")
24
25
  self.ok_button.setFocus()
25
26
  self.cancel_button = QPushButton("Cancel")
26
27
  self.reset_button = QPushButton("Reset")
27
- button_box.addWidget(self.ok_button)
28
- button_box.addWidget(self.cancel_button)
29
- button_box.addWidget(self.reset_button)
28
+ self.button_box.addWidget(self.ok_button)
29
+ self.button_box.addWidget(self.cancel_button)
30
+ self.button_box.addWidget(self.reset_button)
30
31
  self.reset_button.clicked.connect(self.reset_to_defaults)
31
32
  self.ok_button.clicked.connect(self.accept)
32
33
  self.cancel_button.clicked.connect(self.reject)
33
34
  self.form_layout.addRow(scroll_area)
34
- self.form_layout.addRow(button_box)
35
+ self.form_layout.addRow(self.button_box)
35
36
  QTimer.singleShot(0, self.adjust_dialog_size)
36
37
  self.create_form_content()
37
38
 
@@ -0,0 +1,75 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0718
2
+ from xml.dom import minidom
3
+ from PIL.TiffImagePlugin import IFDRational
4
+ from PySide6.QtWidgets import QLabel, QTextEdit
5
+ from PySide6.QtCore import Qt
6
+ from PySide6.QtGui import QFontDatabase
7
+ from .. algorithms.exif import exif_dict
8
+ from .. gui.config_dialog import ConfigDialog
9
+
10
+
11
+ class ExifData(ConfigDialog):
12
+ def __init__(self, exif, title="EXIF Data", parent=None, show_buttons=True):
13
+ self.exif = exif
14
+ super().__init__(title, parent)
15
+ self.reset_button.setVisible(False)
16
+ self.cancel_button.setVisible(show_buttons)
17
+ if not show_buttons:
18
+ self.ok_button.setFixedWidth(100)
19
+ self.button_box.setAlignment(Qt.AlignCenter)
20
+
21
+ def is_likely_xml(self, text):
22
+ if not isinstance(text, str):
23
+ return False
24
+ text = text.strip()
25
+ return (text.startswith('<?xml') or
26
+ text.startswith('<x:xmpmeta') or
27
+ text.startswith('<rdf:RDF') or
28
+ text.startswith('<?xpacket') or
29
+ (text.startswith('<') and text.endswith('>') and
30
+ any(tag in text for tag in ['<rdf:', '<xmp:', '<dc:', '<tiff:'])))
31
+
32
+ def prettify_xml(self, xml_string):
33
+ try:
34
+ parsed = minidom.parseString(xml_string)
35
+ pretty_xml = parsed.toprettyxml(indent=" ")
36
+ lines = [line for line in pretty_xml.split('\n') if line.strip()]
37
+ if lines and lines[0].startswith('<?xml version="1.0" ?>'):
38
+ lines = lines[1:]
39
+ return '\n'.join(lines)
40
+ except Exception:
41
+ return xml_string
42
+
43
+ def create_form_content(self):
44
+ if self.exif is None:
45
+ data = {}
46
+ else:
47
+ data = exif_dict(self.exif)
48
+ if len(data) > 0:
49
+ for k, (_, d) in data.items():
50
+ if isinstance(d, IFDRational):
51
+ d = f"{d.numerator}/{d.denominator}"
52
+ d_str = str(d)
53
+ if "<<<" not in d_str and k != 'IPTCNAA':
54
+ if len(d_str) <= 40:
55
+ self.container_layout.addRow(f"<b>{k}:</b>", QLabel(d_str))
56
+ else:
57
+ if self.is_likely_xml(d_str):
58
+ d_str = self.prettify_xml(d_str)
59
+ text_edit = QTextEdit()
60
+ text_edit.setPlainText(d_str)
61
+ text_edit.setReadOnly(True)
62
+ text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
63
+ text_edit.setLineWrapMode(QTextEdit.WidgetWidth)
64
+ text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
65
+ text_edit.setFixedWidth(400)
66
+ font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
67
+ font.setPointSize(10)
68
+ text_edit.setFont(font)
69
+ font.setPointSize(11)
70
+ text_edit.setFont(font)
71
+ text_edit.setFixedHeight(200)
72
+ text_edit.setFixedHeight(100)
73
+ self.container_layout.addRow(f"<b>{k}:</b>", text_edit)
74
+ else:
75
+ self.container_layout.addRow("No EXIF Data", QLabel(''))
@@ -1,7 +1,8 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915, R0904, W0108
2
2
  from functools import partial
3
3
  from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QMenu,
4
- QListWidget, QSlider, QMainWindow, QMessageBox)
4
+ QFileDialog, QListWidget, QSlider, QMainWindow, QMessageBox,
5
+ QDialog)
5
6
  from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
6
7
  from PySide6.QtCore import Qt
7
8
  from PySide6.QtGui import QGuiApplication
@@ -9,6 +10,7 @@ from .. config.constants import constants
9
10
  from .. config.app_config import AppConfig
10
11
  from .. config.gui_constants import gui_constants
11
12
  from .. gui.recent_file_manager import RecentFileManager
13
+ from .. algorithms.exif import get_exif
12
14
  from .image_viewer import ImageViewer
13
15
  from .shortcuts_help import ShortcutsHelp
14
16
  from .brush import Brush
@@ -26,6 +28,7 @@ from .white_balance_filter import WhiteBalanceFilter
26
28
  from .vignetting_filter import VignettingFilter
27
29
  from .adjustments import LumiContrastFilter, SaturationVibranceFilter
28
30
  from .transformation_manager import TransfromationManager
31
+ from .exif_data import ExifData
29
32
 
30
33
 
31
34
  class ImageEditorUI(QMainWindow, LayerCollectionHandler):
@@ -183,6 +186,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
183
186
  self.thumbnail_list.setFixedWidth(gui_constants.THUMB_WIDTH)
184
187
  self.thumbnail_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
185
188
  self.thumbnail_list.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
189
+ self.exif_dialog = None
186
190
 
187
191
  def change_layer_item(item):
188
192
  layer_idx = self.thumbnail_list.row(item)
@@ -266,8 +270,17 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
266
270
 
267
271
  file_menu.addAction("&Close", self.close_file, "Ctrl+W")
268
272
  file_menu.addSeparator()
273
+ show_exif_action = QAction("Show EXIF Data", self)
274
+ show_exif_action.triggered.connect(self.show_exif_data)
275
+ show_exif_action.setProperty("requires_file", True)
276
+ file_menu.addAction(show_exif_action)
277
+ delete_exif_action = QAction("Delete EXIF Data", self)
278
+ delete_exif_action.triggered.connect(self.delete_exif_data)
279
+ delete_exif_action.setProperty("requires_file", True)
280
+ file_menu.addAction(delete_exif_action)
281
+ file_menu.addSeparator()
269
282
  file_menu.addAction("&Import Frames", self.io_gui_handler.import_frames)
270
- file_menu.addAction("Import &EXIF Data", self.io_gui_handler.select_exif_path)
283
+ file_menu.addAction("Import &EXIF Data", self.select_exif_path)
271
284
 
272
285
  edit_menu = menubar.addMenu("&Edit")
273
286
  self.undo_action = QAction("Undo", self)
@@ -676,6 +689,36 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
676
689
  self.redo_action.setText("Redo")
677
690
  self.redo_action.setEnabled(False)
678
691
 
692
+ def select_exif_path(self):
693
+ path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
694
+ if path:
695
+ temp_exif_data = get_exif(path)
696
+ self.exif_dialog = ExifData(temp_exif_data, "Import Selected EXIF Data",
697
+ self.parent(), show_buttons=True)
698
+ result = self.exif_dialog.exec()
699
+ if result == QDialog.Accepted:
700
+ self.io_gui_handler.set_exif_data(temp_exif_data, path)
701
+ self.show_status_message(f"EXIF data loaded from {path}.")
702
+ else:
703
+ self.show_status_message("EXIF data loading cancelled.")
704
+
705
+ def show_exif_data(self):
706
+ self.exif_dialog = ExifData(self.io_gui_handler.exif_data, "EXIF Data",
707
+ self.parent(), show_buttons=False)
708
+ self.exif_dialog.exec()
709
+
710
+ def delete_exif_data(self):
711
+ reply = QMessageBox.question(
712
+ self,
713
+ "Confirm Delete",
714
+ "Warning: the current EXIF data will be erased.\n\nDo you want to continue?",
715
+ QMessageBox.Yes | QMessageBox.No,
716
+ QMessageBox.No
717
+ )
718
+ if reply == QMessageBox.Yes:
719
+ self.io_gui_handler.exif_data = None
720
+ self.io_gui_handler.exif_path = ''
721
+
679
722
  def luminosity_filter(self):
680
723
  self.filter_manager.apply("Luminosity, Contrast")
681
724
 
@@ -10,7 +10,6 @@ from PySide6.QtCore import Qt, QObject, QTimer, Signal
10
10
  from .. algorithms.utils import EXTENSIONS_GUI_STR, EXTENSIONS_GUI_SAVE_STR
11
11
  from .. algorithms.exif import get_exif, write_image_with_exif_data
12
12
  from .file_loader import FileLoader
13
- from .exif_data import ExifData
14
13
  from .io_threads import FileMultilayerSaver, FrameImporter
15
14
  from .layer_collection import LayerCollectionHandler
16
15
 
@@ -33,7 +32,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
33
32
  self.image_viewer = None
34
33
  self.loading_dialog = None
35
34
  self.loading_timer = None
36
- self.exif_dialog = None
37
35
  self.saver_thread = None
38
36
  self.saving_dialog = None
39
37
  self.saving_timer = None
@@ -47,6 +45,10 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
47
45
  self.exif_data = None
48
46
  self.exif_path = ''
49
47
 
48
+ def set_exif_data(self, data, path):
49
+ self.exif_data = data
50
+ self.exif_path = path
51
+
50
52
  def current_file_path(self):
51
53
  return self.current_file_path_master if self.save_master_only.isChecked() \
52
54
  else self.current_file_path_multi
@@ -164,6 +166,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
164
166
  self.loader_thread.finished.connect(self.on_file_loaded)
165
167
  self.loader_thread.error.connect(self.on_file_error)
166
168
  self.loader_thread.start()
169
+ self.exif_path = self.current_file_path_master
170
+ self.exif_data = get_exif(self.exif_path)
167
171
 
168
172
  def import_frames(self):
169
173
  file_paths, _ = QFileDialog.getOpenFileNames(
@@ -196,6 +200,9 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
196
200
  self.frame_importer_thread.error.connect(self.on_frames_import_error)
197
201
  self.frame_importer_thread.progress.connect(self.update_import_progress)
198
202
  self.frame_importer_thread.start()
203
+ if self.exif_data is None:
204
+ self.exif_path = file_paths[0]
205
+ self.exif_data = get_exif(self.exif_path)
199
206
 
200
207
  def update_import_progress(self, percent, filename):
201
208
  if hasattr(self, 'progress_bar'):
@@ -304,15 +311,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
304
311
  traceback.print_tb(e.__traceback__)
305
312
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
306
313
 
307
- def select_exif_path(self):
308
- path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
309
- if path:
310
- self.exif_path = path
311
- self.exif_data = get_exif(path)
312
- self.status_message_requested.emit(f"EXIF data extracted from {path}.")
313
- self.exif_dialog = ExifData(self.exif_data, self.parent())
314
- self.exif_dialog.exec()
315
-
316
314
  def close_file(self):
317
315
  self.mark_as_modified_requested.emit(False)
318
316
  self.layer_collection.reset()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.9.0
3
+ Version: 1.9.1
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -1 +0,0 @@
1
- __version__ = '1.9.0'