shinestacker 1.8.1__tar.gz → 1.9.0__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 (172) hide show
  1. {shinestacker-1.8.1 → shinestacker-1.9.0}/CHANGELOG.md +13 -0
  2. {shinestacker-1.8.1/src/shinestacker.egg-info → shinestacker-1.9.0}/PKG-INFO +3 -1
  3. {shinestacker-1.8.1 → shinestacker-1.9.0}/README.md +3 -1
  4. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/gui.md +13 -13
  5. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/main.md +2 -1
  6. shinestacker-1.9.0/src/shinestacker/_version.py +1 -0
  7. shinestacker-1.9.0/src/shinestacker/algorithms/exif.py +486 -0
  8. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/multilayer.py +6 -4
  9. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/stack.py +25 -13
  10. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/stack_framework.py +2 -2
  11. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/utils.py +18 -2
  12. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/vignetting.py +1 -1
  13. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/config/constants.py +0 -1
  14. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/action_config_dialog.py +2 -1
  15. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/folder_file_selection.py +3 -2
  16. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/gui_run.py +2 -2
  17. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/new_project.py +5 -5
  18. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/exif_data.py +3 -0
  19. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/file_loader.py +3 -3
  20. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/io_gui_handler.py +4 -4
  21. {shinestacker-1.8.1 → shinestacker-1.9.0/src/shinestacker.egg-info}/PKG-INFO +3 -1
  22. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker.egg-info/SOURCES.txt +0 -1
  23. shinestacker-1.8.1/img/gui-finder.png +0 -0
  24. shinestacker-1.8.1/src/shinestacker/_version.py +0 -1
  25. shinestacker-1.8.1/src/shinestacker/algorithms/exif.py +0 -240
  26. {shinestacker-1.8.1 → shinestacker-1.9.0}/.coveragerc +0 -0
  27. {shinestacker-1.8.1 → shinestacker-1.9.0}/.flake8 +0 -0
  28. {shinestacker-1.8.1 → shinestacker-1.9.0}/.github/workflows/ci-multiplatform.yml +0 -0
  29. {shinestacker-1.8.1 → shinestacker-1.9.0}/.github/workflows/pylint.yml +0 -0
  30. {shinestacker-1.8.1 → shinestacker-1.9.0}/.github/workflows/pypi-publish.yml +0 -0
  31. {shinestacker-1.8.1 → shinestacker-1.9.0}/.github/workflows/release.yml +0 -0
  32. {shinestacker-1.8.1 → shinestacker-1.9.0}/.gitignore +0 -0
  33. {shinestacker-1.8.1 → shinestacker-1.9.0}/.pylintrc +0 -0
  34. {shinestacker-1.8.1 → shinestacker-1.9.0}/.readthedocs.yaml +0 -0
  35. {shinestacker-1.8.1 → shinestacker-1.9.0}/LICENSE +0 -0
  36. {shinestacker-1.8.1 → shinestacker-1.9.0}/MANIFEST.in +0 -0
  37. {shinestacker-1.8.1 → shinestacker-1.9.0}/THIRD_PARTY_LICENSES.txt +0 -0
  38. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/alignment.md +0 -0
  39. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/api.md +0 -0
  40. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/balancing.md +0 -0
  41. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/conf.py +0 -0
  42. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/focus_stacking.md +0 -0
  43. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/index.md +0 -0
  44. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/job.md +0 -0
  45. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/macos-install.md +0 -0
  46. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/multilayer.md +0 -0
  47. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/noise.md +0 -0
  48. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/requirements.txt +0 -0
  49. {shinestacker-1.8.1 → shinestacker-1.9.0}/docs/vignetting.md +0 -0
  50. {shinestacker-1.8.1 → shinestacker-1.9.0}/img/coffee.gif +0 -0
  51. {shinestacker-1.8.1 → shinestacker-1.9.0}/img/coffee_stack.jpg +0 -0
  52. {shinestacker-1.8.1 → shinestacker-1.9.0}/img/extreme-vignetting.jpg +0 -0
  53. {shinestacker-1.8.1 → shinestacker-1.9.0}/img/flies.gif +0 -0
  54. {shinestacker-1.8.1 → shinestacker-1.9.0}/img/flies_stack.jpg +0 -0
  55. {shinestacker-1.8.1 → shinestacker-1.9.0}/img/flow-diagram.png +0 -0
  56. {shinestacker-1.8.1 → shinestacker-1.9.0}/img/gui-project-new.png +0 -0
  57. {shinestacker-1.8.1 → shinestacker-1.9.0}/img/gui-project-run.png +0 -0
  58. {shinestacker-1.8.1 → shinestacker-1.9.0}/img/gui-retouch.png +0 -0
  59. {shinestacker-1.8.1 → shinestacker-1.9.0}/index.html +0 -0
  60. {shinestacker-1.8.1 → shinestacker-1.9.0}/pyproject.toml +0 -0
  61. {shinestacker-1.8.1 → shinestacker-1.9.0}/requirements.txt +0 -0
  62. {shinestacker-1.8.1 → shinestacker-1.9.0}/scripts/build_release.py +0 -0
  63. {shinestacker-1.8.1 → shinestacker-1.9.0}/scripts/create_macos_icon.py +0 -0
  64. {shinestacker-1.8.1 → shinestacker-1.9.0}/scripts/git-rev-list.sh +0 -0
  65. {shinestacker-1.8.1 → shinestacker-1.9.0}/scripts/hooks/hook-IPython.py +0 -0
  66. {shinestacker-1.8.1 → shinestacker-1.9.0}/scripts/hooks/hook-PySide6.py +0 -0
  67. {shinestacker-1.8.1 → shinestacker-1.9.0}/scripts/hooks/hook-opencv.py +0 -0
  68. {shinestacker-1.8.1 → shinestacker-1.9.0}/scripts/hooks/hook-tests.py +0 -0
  69. {shinestacker-1.8.1 → shinestacker-1.9.0}/scripts/scan_imports.py +0 -0
  70. {shinestacker-1.8.1 → shinestacker-1.9.0}/scripts/shinestacker-inno-setup.iss +0 -0
  71. {shinestacker-1.8.1 → shinestacker-1.9.0}/scripts/validate-tomli.py +0 -0
  72. {shinestacker-1.8.1 → shinestacker-1.9.0}/setup.cfg +0 -0
  73. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/__init__.py +0 -0
  74. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/__init__.py +0 -0
  75. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/align.py +0 -0
  76. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/align_auto.py +0 -0
  77. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/align_parallel.py +0 -0
  78. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/balance.py +0 -0
  79. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/base_stack_algo.py +0 -0
  80. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/corrections.py +0 -0
  81. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/denoise.py +0 -0
  82. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/depth_map.py +0 -0
  83. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/noise_detection.py +0 -0
  84. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/pyramid.py +0 -0
  85. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/pyramid_auto.py +0 -0
  86. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/pyramid_tiles.py +0 -0
  87. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/sharpen.py +0 -0
  88. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/algorithms/white_balance.py +0 -0
  89. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/app/__init__.py +0 -0
  90. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/app/about_dialog.py +0 -0
  91. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/app/args_parser_opts.py +0 -0
  92. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/app/gui_utils.py +0 -0
  93. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/app/help_menu.py +0 -0
  94. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/app/main.py +0 -0
  95. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/app/open_frames.py +0 -0
  96. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/app/project.py +0 -0
  97. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/app/retouch.py +0 -0
  98. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/app/settings_dialog.py +0 -0
  99. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/config/__init__.py +0 -0
  100. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/config/app_config.py +0 -0
  101. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/config/config.py +0 -0
  102. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/config/gui_constants.py +0 -0
  103. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/config/settings.py +0 -0
  104. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/core/__init__.py +0 -0
  105. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/core/colors.py +0 -0
  106. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/core/core_utils.py +0 -0
  107. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/core/exceptions.py +0 -0
  108. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/core/framework.py +0 -0
  109. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/core/logging.py +0 -0
  110. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/__init__.py +0 -0
  111. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/action_config.py +0 -0
  112. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/base_form_dialog.py +0 -0
  113. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/colors.py +0 -0
  114. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/config_dialog.py +0 -0
  115. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/flow_layout.py +0 -0
  116. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/gui_images.py +0 -0
  117. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/gui_logging.py +0 -0
  118. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/ico/shinestacker.icns +0 -0
  119. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/ico/shinestacker.ico +0 -0
  120. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/ico/shinestacker.png +0 -0
  121. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/ico/shinestacker.svg +0 -0
  122. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/img/dark/close-round-line-icon.png +0 -0
  123. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/img/dark/forward-button-icon.png +0 -0
  124. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/img/dark/play-button-round-icon.png +0 -0
  125. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/img/dark/plus-round-line-icon.png +0 -0
  126. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/img/dark/shinestacker_bkg.png +0 -0
  127. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/img/light/close-round-line-icon.png +0 -0
  128. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/img/light/forward-button-icon.png +0 -0
  129. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/img/light/play-button-round-icon.png +0 -0
  130. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/img/light/plus-round-line-icon.png +0 -0
  131. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/img/light/shinestacker_bkg.png +0 -0
  132. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/main_window.py +0 -0
  133. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/menu_manager.py +0 -0
  134. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/project_controller.py +0 -0
  135. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/project_converter.py +0 -0
  136. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/project_editor.py +0 -0
  137. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/project_model.py +0 -0
  138. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/recent_file_manager.py +0 -0
  139. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/select_path_widget.py +0 -0
  140. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/sys_mon.py +0 -0
  141. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/tab_widget.py +0 -0
  142. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/gui/time_progress_bar.py +0 -0
  143. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/__init__.py +0 -0
  144. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/adjustments.py +0 -0
  145. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/base_filter.py +0 -0
  146. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/brush.py +0 -0
  147. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/brush_gradient.py +0 -0
  148. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/brush_preview.py +0 -0
  149. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/brush_tool.py +0 -0
  150. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/denoise_filter.py +0 -0
  151. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/display_manager.py +0 -0
  152. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/filter_manager.py +0 -0
  153. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/icon_container.py +0 -0
  154. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/image_editor_ui.py +0 -0
  155. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/image_view_status.py +0 -0
  156. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/image_viewer.py +0 -0
  157. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/io_threads.py +0 -0
  158. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/layer_collection.py +0 -0
  159. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/overlaid_view.py +0 -0
  160. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/paint_area_manager.py +0 -0
  161. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/shortcuts_help.py +0 -0
  162. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/sidebyside_view.py +0 -0
  163. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/transformation_manager.py +0 -0
  164. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/undo_manager.py +0 -0
  165. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/unsharp_mask_filter.py +0 -0
  166. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/view_strategy.py +0 -0
  167. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/vignetting_filter.py +0 -0
  168. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker/retouch/white_balance_filter.py +0 -0
  169. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker.egg-info/dependency_links.txt +0 -0
  170. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker.egg-info/entry_points.txt +0 -0
  171. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker.egg-info/requires.txt +0 -0
  172. {shinestacker-1.8.1 → shinestacker-1.9.0}/src/shinestacker.egg-info/top_level.txt +0 -0
@@ -2,6 +2,19 @@
2
2
 
3
3
  This page reports the main releases only and the main changes therein.
4
4
 
5
+
6
+ ## [v1.9.0] - 2025-10-19
7
+ ** Added PNG format support and EXIF failure fix**
8
+
9
+ ### Added
10
+ - 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.
11
+
12
+ ### Fixed
13
+ - if saving EXIF data fails, a warning is issued instead of stopping the run
14
+
15
+ -----
16
+
17
+
5
18
  ## [v1.8.1] - 2025-10-16
6
19
  ** Alignment stability and performance improvements **
7
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.8.1
3
+ Version: 1.9.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -105,11 +105,13 @@ Pyramid methods in image processing
105
105
  - **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista). Copyright © Alessandro Lista. All rights reserved. The logo is not covered by the LGPL-3.0 license of this project.
106
106
 
107
107
  ## Attribution request
108
+
108
109
  📸 If you publish images created with Shine Stacker, please consider adding a note such as:
109
110
 
110
111
  *Created with Shine Stacker – https://github.com/lucalista/shinestacker*
111
112
 
112
113
  This is not mandatory, but highly appreciated.
114
+
113
115
  ---
114
116
  > Developed and maintained by [Luca Lista](https://github.com/lucalista).
115
117
  > 💡 Contributions, feedback, and feature suggestions are warmly welcome.
@@ -73,12 +73,14 @@ Pyramid methods in image processing
73
73
  - **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista). Copyright © Alessandro Lista. All rights reserved. The logo is not covered by the LGPL-3.0 license of this project.
74
74
 
75
75
  ## Attribution request
76
+
76
77
  📸 If you publish images created with Shine Stacker, please consider adding a note such as:
77
78
 
78
79
  *Created with Shine Stacker – https://github.com/lucalista/shinestacker*
79
80
 
80
81
  This is not mandatory, but highly appreciated.
82
+
81
83
  ---
82
84
  > Developed and maintained by [Luca Lista](https://github.com/lucalista).
83
85
  > 💡 Contributions, feedback, and feature suggestions are warmly welcome.
84
- > If you enjoy Shine Stacker, consider giving it a ⭐️ on GitHub — it really helps visibility!
86
+ > If you enjoy Shine Stacker, consider giving it a ⭐️ on GitHub — it really helps visibility!
@@ -12,24 +12,28 @@ noisy pixel masking.
12
12
 
13
13
  ## Starting
14
14
 
15
- * If the python package is donwloaded and installed, the GUI can start either from a console command line :
15
+ * The python package can be installed from [PyPI](https://pypi.org/project/shinestacker/) using ```pip```:
16
16
 
17
17
  ```console
18
- > focusstack
18
+ > pip install shinestacker
19
19
  ```
20
+ Onace installed, the GUI app can start either from a console command line :
20
21
 
21
- * If the app is dowloaded from the [releases page](https://github.com/lucalista/focusstack/releases), after the ```zip``` archive is uncompressed, just double-click the app icon.
22
-
23
- <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-finder.png' width="300" referrerpolicy="no-referrer">
22
+ ```console
23
+ > shinestacker
24
+ ```
24
25
 
25
- **Platform Tip**: Windows apps are inside `/focusstack/`, macOS/Linux apps are directly in the uncompressed folder.
26
+ * The app can be dowloaded from the [releases page](https://github.com/lucalista/shinestacker/releases):
27
+ - Windows: as installer or ```zip``` archive for local installation
28
+ - macOS: as ```dmg``` disk image, where the app can be dragged to the Application folder
29
+ - Linux: as ```tar.gz``` archive
26
30
 
27
31
  The GUI has two main working areas:
28
32
 
29
33
  * *Project*
30
34
  * *Retouch*
31
35
 
32
- Switching from *Project* to *Retouch* can be done from the *FocusStack* main menu.
36
+ Switching from *Project* to *Retouch* can be done from the *ShineStacker* main menu.
33
37
 
34
38
  ## Project area
35
39
 
@@ -48,17 +52,13 @@ When the app starts, it proposes to create a new project.
48
52
 
49
53
  > **Large Set Tip**: For 100+ images:
50
54
  > - Split into 10-15 image "bunches"
51
- > - Set frame overlap (default: 2 frames)
55
+ > - Set number of overlapping frame from consecutive bunches
52
56
  > - Combine intermediate results later
53
57
 
54
- > 💡 **RAM Warning**: >15 images of 20Mpx resolution may need 16GB+ RAM. Combine smaller bunches first, if needed, to stack up to hundreds of frames.
55
-
56
58
  The newly created project consists of a single job that contains more actions.
57
59
  Each action produces a folder as output that has, by default, the action's name.
58
60
  Some actions can be combined in order to produce a single intermediate output (alignment, balancing, etc.).
59
61
 
60
- **Action Outputs**: 📁 `aligned-balanced/` | 📁 `bunches/` | 📁 `stacked/`
61
-
62
62
  > **Pro Tip**: Duplicate jobs when processing similar image sets to save configuration time. You can run multiple jobs in sequence.
63
63
 
64
64
  It is possible to run a single job, or all jobs within a project.
@@ -67,7 +67,7 @@ It is possible to run a single job, or all jobs within a project.
67
67
 
68
68
  ### Project Run Tabs
69
69
 
70
- 1. Job progress bar
70
+ 1. Job progress bar with CPU and RAM usage monitor
71
71
  2. Real-time log viewer
72
72
  3. Retouch button (enabled after processing)
73
73
 
@@ -103,5 +103,6 @@ pip install ipywidgets
103
103
 
104
104
  | Issue | Workaround |
105
105
  |----------|----------------|
106
- | PNG and RAW unsupported | Convert to TIFF/JPEG first |
106
+ | RAW format unsupported | Convert to TIFF/JPEG first |
107
+ | EXIF data not supported for 16-bit PNG files | convert to 16-bit TIFF first |
107
108
  | GUI tests limited | Report any bugs as GitHub issuse |
@@ -0,0 +1 @@
1
+ __version__ = '1.9.0'
@@ -0,0 +1,486 @@
1
+ # pylint: disable=C0114, C0116, W0718, R0911, R0912, E1101, R0915, R1702, R0914, R0917, R0913
2
+ import os
3
+ import re
4
+ import io
5
+ import logging
6
+ import traceback
7
+ import cv2
8
+ import numpy as np
9
+ from PIL import Image
10
+ from PIL.TiffImagePlugin import IFDRational
11
+ from PIL.PngImagePlugin import PngInfo
12
+ from PIL.ExifTags import TAGS
13
+ import tifffile
14
+ from .. config.constants import constants
15
+ from .utils import write_img, extension_jpg, extension_tif, extension_png
16
+
17
+ IMAGEWIDTH = 256
18
+ IMAGELENGTH = 257
19
+ RESOLUTIONX = 282
20
+ RESOLUTIONY = 283
21
+ RESOLUTIONUNIT = 296
22
+ BITSPERSAMPLE = 258
23
+ PHOTOMETRICINTERPRETATION = 262
24
+ SAMPLESPERPIXEL = 277
25
+ PLANARCONFIGURATION = 284
26
+ SOFTWARE = 305
27
+ IMAGERESOURCES = 34377
28
+ INTERCOLORPROFILE = 34675
29
+ EXIFTAG = 34665
30
+ XMLPACKET = 700
31
+ STRIPOFFSETS = 273
32
+ STRIPBYTECOUNTS = 279
33
+ NO_COPY_TIFF_TAGS_ID = [IMAGEWIDTH, IMAGELENGTH, RESOLUTIONX, RESOLUTIONY, BITSPERSAMPLE,
34
+ PHOTOMETRICINTERPRETATION, SAMPLESPERPIXEL, PLANARCONFIGURATION, SOFTWARE,
35
+ RESOLUTIONUNIT, EXIFTAG, INTERCOLORPROFILE, IMAGERESOURCES]
36
+ NO_COPY_TIFF_TAGS = ["Compression", "StripOffsets", "RowsPerStrip", "StripByteCounts"]
37
+
38
+
39
+ 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
48
+
49
+
50
+ def get_exif(exif_filename):
51
+ if not os.path.isfile(exif_filename):
52
+ raise RuntimeError(f"File does not exist: {exif_filename}")
53
+ image = Image.open(exif_filename)
54
+ if extension_tif(exif_filename):
55
+ return image.tag_v2 if hasattr(image, 'tag_v2') else image.getexif()
56
+ if extension_jpg(exif_filename):
57
+ exif_data = image.getexif()
58
+ with open(exif_filename, 'rb') as f:
59
+ data = extract_enclosed_data_for_jpg(f.read(), b'<?xpacket', b'<?xpacket end="w"?>')
60
+ if data is not None:
61
+ exif_data[XMLPACKET] = data
62
+ return exif_data
63
+ if extension_png(exif_filename):
64
+ exif_data = get_exif_from_png(image)
65
+ return exif_data if exif_data else image.getexif()
66
+ return image.getexif()
67
+
68
+
69
+ def get_exif_from_png(image):
70
+ exif_data = {}
71
+ try:
72
+ exif_from_image = image.getexif()
73
+ if exif_from_image:
74
+ exif_data.update(dict(exif_from_image))
75
+ except Exception:
76
+ pass
77
+ try:
78
+ if hasattr(image, 'text') and image.text:
79
+ for key, value in image.text.items():
80
+ exif_data[f"PNG_{key}"] = value
81
+ if hasattr(image, 'info') and image.info:
82
+ for key, value in image.info.items():
83
+ if key not in ['dpi', 'gamma']:
84
+ exif_data[f"PNG_{key}"] = value
85
+ except Exception:
86
+ pass
87
+ return exif_data
88
+
89
+
90
+ def exif_extra_tags_for_tif(exif):
91
+ logger = logging.getLogger(__name__)
92
+ res_x, res_y = exif.get(RESOLUTIONX), exif.get(RESOLUTIONY)
93
+ if not (res_x is None or res_y is None):
94
+ resolution = ((res_x.numerator, res_x.denominator), (res_y.numerator, res_y.denominator))
95
+ else:
96
+ resolution = ((720000, 10000), (720000, 10000))
97
+ res_u = exif.get(RESOLUTIONUNIT)
98
+ resolutionunit = res_u if res_u is not None else 'inch'
99
+ sw = exif.get(SOFTWARE)
100
+ software = sw if sw is not None else "N/A"
101
+ phint = exif.get(PHOTOMETRICINTERPRETATION)
102
+ photometric = phint if phint is not None else None
103
+ extra = []
104
+ for tag_id in exif:
105
+ tag, data = TAGS.get(tag_id, tag_id), exif.get(tag_id)
106
+ if isinstance(data, bytes):
107
+ try:
108
+ if tag_id not in (IMAGERESOURCES, INTERCOLORPROFILE):
109
+ if tag_id == XMLPACKET:
110
+ data = re.sub(b'[^\x20-\x7E]', b'', data)
111
+ data = data.decode()
112
+ except Exception:
113
+ logger.warning(msg=f"Copy: can't decode EXIF tag {tag:25} [#{tag_id}]")
114
+ data = '<<< decode error >>>'
115
+ if isinstance(data, IFDRational):
116
+ data = (data.numerator, data.denominator)
117
+ if tag not in NO_COPY_TIFF_TAGS and tag_id not in NO_COPY_TIFF_TAGS_ID:
118
+ extra.append((tag_id, *get_tiff_dtype_count(data), data, False))
119
+ else:
120
+ logger.debug(msg=f"Skip tag {tag:25} [#{tag_id}]")
121
+ return extra, {'resolution': resolution, 'resolutionunit': resolutionunit,
122
+ 'software': software, 'photometric': photometric}
123
+
124
+
125
+ def get_tiff_dtype_count(value):
126
+ if isinstance(value, str):
127
+ return 2, len(value) + 1 # ASCII string, (dtype=2), length + null terminator
128
+ if isinstance(value, (bytes, bytearray)):
129
+ return 1, len(value) # Binary data (dtype=1)
130
+ if isinstance(value, (list, tuple, np.ndarray)):
131
+ if isinstance(value, np.ndarray):
132
+ dtype = value.dtype # Array or sequence
133
+ else:
134
+ dtype = np.array(value).dtype # Map numpy dtype to TIFF dtype
135
+ if dtype == np.uint8:
136
+ return 1, len(value)
137
+ if dtype == np.uint16:
138
+ return 3, len(value)
139
+ if dtype == np.uint32:
140
+ return 4, len(value)
141
+ if dtype == np.float32:
142
+ return 11, len(value)
143
+ if dtype == np.float64:
144
+ return 12, len(value)
145
+ if isinstance(value, int):
146
+ if 0 <= value <= 65535:
147
+ return 3, 1 # uint16
148
+ return 4, 1 # uint32
149
+ if isinstance(value, float):
150
+ return 11, 1 # float64
151
+ return 2, len(str(value)) + 1 # Default for othre cases (ASCII string)
152
+
153
+
154
+ def add_exif_data_to_jpg_file(exif, in_filenama, out_filename, verbose=False):
155
+ logger = logging.getLogger(__name__)
156
+ if exif is None:
157
+ raise RuntimeError('No exif data provided.')
158
+ if verbose:
159
+ 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
180
+
181
+
182
+ def create_xmp_from_exif(exif_data):
183
+ xmp_elements = []
184
+ if exif_data:
185
+ for tag_id, value in exif_data.items():
186
+ if isinstance(tag_id, int):
187
+ if tag_id == 270 and value: # ImageDescription
188
+ desc = value
189
+ if isinstance(desc, bytes):
190
+ desc = desc.decode('utf-8', errors='ignore')
191
+ xmp_elements.append(
192
+ f'<dc:description><rdf:Alt><rdf:li xml:lang="x-default">{desc}</rdf:li>'
193
+ '</rdf:Alt></dc:description>')
194
+ elif tag_id == 315 and value: # Artist
195
+ artist = value
196
+ if isinstance(artist, bytes):
197
+ artist = artist.decode('utf-8', errors='ignore')
198
+ xmp_elements.append(
199
+ f'<dc:creator><rdf:Seq><rdf:li>{artist}</rdf:li>'
200
+ '</rdf:Seq></dc:creator>')
201
+ elif tag_id == 33432 and value: # Copyright
202
+ copyright_tag = value
203
+ if isinstance(copyright_tag, bytes):
204
+ copyright_tag = copyright_tag.decode('utf-8', errors='ignore')
205
+ xmp_elements.append(
206
+ f'<dc:rights><rdf:Alt><rdf:li xml:lang="x-default">{copyright_tag}</rdf:li>'
207
+ '</rdf:Alt></dc:rights>')
208
+ elif tag_id == 271 and value: # Make
209
+ make = value
210
+ if isinstance(make, bytes):
211
+ make = make.decode('utf-8', errors='ignore')
212
+ xmp_elements.append(f'<tiff:Make>{make}</tiff:Make>')
213
+ elif tag_id == 272 and value: # Model
214
+ model = value
215
+ if isinstance(model, bytes):
216
+ model = model.decode('utf-8', errors='ignore')
217
+ xmp_elements.append(f'<tiff:Model>{model}</tiff:Model>')
218
+ elif tag_id == 306 and value: # DateTime
219
+ datetime_val = value
220
+ if isinstance(datetime_val, bytes):
221
+ datetime_val = datetime_val.decode('utf-8', errors='ignore')
222
+ if ':' in datetime_val:
223
+ datetime_val = datetime_val.replace(':', '-', 2).replace(' ', 'T')
224
+ xmp_elements.append(f'<xmp:CreateDate>{datetime_val}</xmp:CreateDate>')
225
+ elif tag_id == 305 and value: # Software
226
+ software = value
227
+ if isinstance(software, bytes):
228
+ software = software.decode('utf-8', errors='ignore')
229
+ xmp_elements.append(f'<xmp:CreatorTool>{software}</xmp:CreatorTool>')
230
+ if xmp_elements:
231
+ xmp_content = '\n '.join(xmp_elements)
232
+ xmp_template = f"""<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
233
+ <x:xmpmeta xmlns:x='adobe:ns:meta/'
234
+ x:xmptk='Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21'>
235
+ <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
236
+ <rdf:Description rdf:about=''
237
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
238
+ xmlns:xmp='http://ns.adobe.com/xap/1.0/'
239
+ xmlns:tiff='http://ns.adobe.com/tiff/1.0/'
240
+ xmlns:exif='http://ns.adobe.com/exif/1.0/'>
241
+ {xmp_content}
242
+ </rdf:Description>
243
+ </rdf:RDF>
244
+ </x:xmpmeta>
245
+ <?xpacket end='w'?>"""
246
+ return xmp_template
247
+ return """<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
248
+ <x:xmpmeta xmlns:x='adobe:ns:meta/'
249
+ x:xmptk='Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21'>
250
+ <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
251
+ <rdf:Description rdf:about=''/>
252
+ </rdf:RDF>
253
+ </x:xmpmeta>
254
+ <?xpacket end='w'?>"""
255
+
256
+
257
+ def write_image_with_exif_data_png(exif, image, out_filename, verbose=False, color_order='auto'):
258
+ logger = logging.getLogger(__name__)
259
+ if isinstance(image, np.ndarray) and image.dtype == np.uint16:
260
+ if verbose:
261
+ logger.warning(msg="EXIF data not supported for 16-bit PNG format")
262
+ write_img(out_filename, image)
263
+ return
264
+ pil_image = _convert_to_pil_image(image, color_order, verbose, logger)
265
+ pnginfo, icc_profile = _prepare_png_metadata(exif, verbose, logger)
266
+ _save_png_with_metadata(pil_image, out_filename, pnginfo, icc_profile, verbose, logger)
267
+
268
+
269
+ def _convert_to_pil_image(image, color_order, verbose, logger):
270
+ if isinstance(image, np.ndarray):
271
+ if len(image.shape) == 3 and image.shape[2] == 3:
272
+ if color_order in ['auto', 'bgr']:
273
+ image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
274
+ if verbose:
275
+ logger.info(msg="Converted BGR to RGB for PIL")
276
+ return Image.fromarray(image_rgb)
277
+ return Image.fromarray(image)
278
+ return image
279
+
280
+
281
+ def _prepare_png_metadata(exif, verbose, logger):
282
+ pnginfo = PngInfo()
283
+ icc_profile = None
284
+ xmp_data = _extract_xmp_data(exif, verbose, logger)
285
+ if xmp_data:
286
+ pnginfo.add_text("XML:com.adobe.xmp", xmp_data)
287
+ if verbose:
288
+ logger.info(msg="Added XMP data to PNG info")
289
+ _add_exif_tags_to_pnginfo(exif, pnginfo, verbose, logger)
290
+ icc_profile = _extract_icc_profile(exif, verbose, logger)
291
+ return pnginfo, icc_profile
292
+
293
+
294
+ def _extract_xmp_data(exif, verbose, logger):
295
+ for key, value in exif.items():
296
+ if isinstance(key, str) and ('xmp' in key.lower() or 'xml' in key.lower()):
297
+ if isinstance(value, bytes):
298
+ try:
299
+ xmp_data = value.decode('utf-8', errors='ignore')
300
+ if verbose:
301
+ logger.info(msg=f"Found existing XMP data in source: {key}")
302
+ return xmp_data
303
+ except Exception:
304
+ continue
305
+ elif isinstance(value, str):
306
+ if verbose:
307
+ logger.info(msg=f"Found existing XMP data in source: {key}")
308
+ return value
309
+ if verbose:
310
+ logger.info("Generated new XMP data from EXIF")
311
+ return create_xmp_from_exif(exif)
312
+
313
+
314
+ def _add_exif_tags_to_pnginfo(exif, pnginfo, verbose, logger):
315
+ for tag_id, value in exif.items():
316
+ if value is None:
317
+ continue
318
+ if isinstance(tag_id, int):
319
+ _add_exif_tag(pnginfo, tag_id, value, verbose, logger)
320
+ elif isinstance(tag_id, str) and not tag_id.lower().startswith(('xmp', 'xml')):
321
+ _add_png_text_tag(pnginfo, tag_id, value, verbose, logger)
322
+
323
+
324
+ def _add_exif_tag(pnginfo, tag_id, value, verbose, logger):
325
+ try:
326
+ tag_name = TAGS.get(tag_id, f"Unknown_{tag_id}")
327
+ if isinstance(value, bytes) and len(value) > 1000:
328
+ return
329
+ if isinstance(value, (int, float, str)):
330
+ pnginfo.add_text(tag_name, str(value))
331
+ elif isinstance(value, bytes):
332
+ try:
333
+ decoded_value = value.decode('utf-8', errors='replace')
334
+ pnginfo.add_text(tag_name, decoded_value)
335
+ except Exception:
336
+ pass
337
+ elif hasattr(value, 'numerator'): # IFDRational
338
+ rational_str = f"{value.numerator}/{value.denominator}"
339
+ pnginfo.add_text(tag_name, rational_str)
340
+ else:
341
+ pnginfo.add_text(tag_name, str(value))
342
+ except Exception as e:
343
+ if verbose:
344
+ logger.warning(f"Could not store EXIF tag {tag_id}: {e}")
345
+
346
+
347
+ def _add_png_text_tag(pnginfo, key, value, verbose, logger):
348
+ try:
349
+ clean_key = key[4:] if key.startswith('PNG_') else key
350
+ if 'icc' in clean_key.lower() or 'profile' in clean_key.lower():
351
+ return
352
+ if isinstance(value, bytes):
353
+ try:
354
+ decoded_value = value.decode('utf-8', errors='replace')
355
+ pnginfo.add_text(clean_key, decoded_value)
356
+ except Exception:
357
+ truncated_value = str(value)[:100] + "..."
358
+ pnginfo.add_text(clean_key, truncated_value)
359
+ else:
360
+ pnginfo.add_text(clean_key, str(value))
361
+ except Exception as e:
362
+ if verbose:
363
+ logger.warning(msg=f"Could not store PNG metadata {key}: {e}")
364
+
365
+
366
+ def _extract_icc_profile(exif, verbose, logger):
367
+ for key, value in exif.items():
368
+ if (isinstance(key, str) and
369
+ isinstance(value, bytes) and
370
+ ('icc' in key.lower() or 'profile' in key.lower())):
371
+ if verbose:
372
+ logger.info(f"Found ICC profile: {key}")
373
+ return value
374
+ return None
375
+
376
+
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')
395
+
396
+
397
+ def write_image_with_exif_data(exif, image, out_filename, verbose=False, color_order='auto'):
398
+ if exif is None:
399
+ write_img(out_filename, image)
400
+ return None
401
+ if verbose:
402
+ print_exif(exif)
403
+ 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)
406
+ 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)
411
+ elif extension_png(out_filename):
412
+ write_image_with_exif_data_png(exif, image, out_filename, verbose, color_order=color_order)
413
+ return exif
414
+
415
+
416
+ def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
417
+ if out_filename is None:
418
+ out_filename = in_filename
419
+ if exif is None:
420
+ raise RuntimeError('No exif data provided.')
421
+ if verbose:
422
+ print_exif(exif)
423
+ if extension_tif(in_filename):
424
+ image_new = tifffile.imread(in_filename)
425
+ elif extension_jpg(in_filename):
426
+ image_new = Image.open(in_filename)
427
+ elif extension_png(in_filename):
428
+ image_new = cv2.imread(in_filename, cv2.IMREAD_UNCHANGED)
429
+ if extension_jpg(in_filename):
430
+ add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose)
431
+ elif extension_tif(in_filename):
432
+ metadata = {"description": f"image generated with {constants.APP_STRING} package"}
433
+ extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
434
+ tifffile.imwrite(out_filename, image_new, metadata=metadata, compression='adobe_deflate',
435
+ extratags=extra_tags, **exif_tags)
436
+ elif extension_png(in_filename):
437
+ write_image_with_exif_data_png(exif, image_new, out_filename, verbose)
438
+ return exif
439
+
440
+
441
+ def copy_exif_from_file_to_file(exif_filename, in_filename, out_filename=None, verbose=False):
442
+ if not os.path.isfile(exif_filename):
443
+ raise RuntimeError(f"File does not exist: {exif_filename}")
444
+ if not os.path.isfile(in_filename):
445
+ raise RuntimeError(f"File does not exist: {in_filename}")
446
+ exif = get_exif(exif_filename)
447
+ return save_exif_data(exif, in_filename, out_filename, verbose)
448
+
449
+
450
+ def exif_dict(exif, hide_xml=True):
451
+ if exif is None:
452
+ return None
453
+ exif_data = {}
454
+ for tag_id in exif:
455
+ 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]
466
+ if isinstance(data, bytes):
467
+ try:
468
+ data = data.decode()
469
+ except Exception:
470
+ pass
471
+ exif_data[tag] = (tag_id, data)
472
+ return exif_data
473
+
474
+
475
+ def print_exif(exif, hide_xml=True):
476
+ exif_data = exif_dict(exif, hide_xml)
477
+ if exif_data is None:
478
+ raise RuntimeError('Image has no exif data.')
479
+ logger = logging.getLogger(__name__)
480
+ for tag, (tag_id, data) in exif_data.items():
481
+ if isinstance(data, IFDRational):
482
+ data = f"{data.numerator}/{data.denominator}"
483
+ if isinstance(tag_id, int):
484
+ logger.info(msg=f"{tag:25} [#{tag_id:5d}]: {data}")
485
+ else:
486
+ logger.info(msg=f"{tag:25} [ {tag_id:20} ]: {str(data)[:100]}...")
@@ -13,7 +13,7 @@ from .. config.constants import constants
13
13
  from .. config.config import config
14
14
  from .. core.colors import color_str
15
15
  from .. core.framework import TaskBase
16
- from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG
16
+ from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG, EXTENSIONS_SUPPORTED
17
17
  from .stack_framework import ImageSequenceManager
18
18
  from .exif import exif_extra_tags_for_tif, get_exif
19
19
 
@@ -142,14 +142,16 @@ def write_multilayer_tiff_from_images(image_dict, output_file, exif_path='', cal
142
142
  elif os.path.isdir(exif_path):
143
143
  _dirpath, _, fnames = next(os.walk(exif_path))
144
144
  fnames = [name for name in fnames
145
- if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
146
- extra_tags, exif_tags = exif_extra_tags_for_tif(get_exif(exif_path + '/' + fnames[0]))
145
+ if os.path.splitext(name)[-1][1:].lower() in EXTENSIONS_SUPPORTED]
146
+ file_path = os.path.join(exif_path, fnames[0])
147
+ extra_tags, exif_tags = exif_extra_tags_for_tif(get_exif(file_path))
148
+ extra_tags = [tag for tag in extra_tags if isinstance(tag[0], int)]
147
149
  tiff_tags['extratags'] += extra_tags
148
150
  tiff_tags = {**tiff_tags, **exif_tags}
149
151
  if callbacks:
150
152
  callback = callbacks.get('write_msg', None)
151
153
  if callback:
152
- callback(output_file.split('/')[-1])
154
+ callback(os.path.basename(output_file))
153
155
  compression = 'adobe_deflate'
154
156
  overlayed_images = overlay(
155
157
  *((np.concatenate((image, np.expand_dims(transp, axis=-1)),