shinestacker 1.9.0__py3-none-any.whl → 1.9.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 shinestacker might be problematic. Click here for more details.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/exif.py +133 -73
- shinestacker/algorithms/stack.py +1 -1
- shinestacker/app/main.py +1 -1
- shinestacker/gui/action_config_dialog.py +0 -4
- shinestacker/gui/config_dialog.py +6 -5
- shinestacker/retouch/exif_data.py +57 -37
- shinestacker/retouch/image_editor_ui.py +45 -2
- shinestacker/retouch/io_gui_handler.py +9 -11
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.1.dist-info}/METADATA +1 -1
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.1.dist-info}/RECORD +15 -15
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.1.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.9.
|
|
1
|
+
__version__ = '1.9.1'
|
shinestacker/algorithms/exif.py
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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,
|
|
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 =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
476
|
-
exif_data = exif_dict(exif
|
|
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
|
-
|
|
543
|
+
tag_id_str = f"[#{tag_id:5d}]"
|
|
485
544
|
else:
|
|
486
|
-
|
|
545
|
+
tag_id_str = f"[ {tag_id:20} ]"
|
|
546
|
+
logger.info(msg=f"{tag:25} {tag_id_str}: {data_str}")
|
shinestacker/algorithms/stack.py
CHANGED
shinestacker/app/main.py
CHANGED
|
@@ -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
|
|
|
@@ -1,55 +1,75 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0718
|
|
2
|
+
from xml.dom import minidom
|
|
2
3
|
from PIL.TiffImagePlugin import IFDRational
|
|
3
|
-
from PySide6.QtWidgets import
|
|
4
|
+
from PySide6.QtWidgets import QLabel, QTextEdit
|
|
4
5
|
from PySide6.QtCore import Qt
|
|
6
|
+
from PySide6.QtGui import QFontDatabase
|
|
5
7
|
from .. algorithms.exif import exif_dict
|
|
6
|
-
from .
|
|
7
|
-
from .. gui.base_form_dialog import BaseFormDialog
|
|
8
|
+
from .. gui.config_dialog import ConfigDialog
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class ExifData(
|
|
11
|
-
def __init__(self, exif, parent=None):
|
|
12
|
-
super().__init__("EXIF data", parent=parent)
|
|
11
|
+
class ExifData(ConfigDialog):
|
|
12
|
+
def __init__(self, exif, title="EXIF Data", parent=None, show_buttons=True):
|
|
13
13
|
self.exif = exif
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
ok_button.setFocus()
|
|
21
|
-
button_layout.addWidget(ok_button)
|
|
22
|
-
self.add_row_to_layout(button_container)
|
|
23
|
-
ok_button.clicked.connect(self.accept)
|
|
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)
|
|
24
20
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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:'])))
|
|
29
31
|
|
|
30
|
-
def
|
|
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
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
spacer.setFixedHeight(10)
|
|
35
|
-
self.form_layout.addRow(spacer)
|
|
36
|
-
self.add_bold_label("EXIF data")
|
|
37
|
-
shortcuts = {}
|
|
43
|
+
def create_form_content(self):
|
|
38
44
|
if self.exif is None:
|
|
39
|
-
shortcuts['Warning:'] = 'no EXIF data found'
|
|
40
45
|
data = {}
|
|
41
46
|
else:
|
|
42
47
|
data = exif_dict(self.exif)
|
|
43
48
|
if len(data) > 0:
|
|
44
49
|
for k, (_, d) in data.items():
|
|
45
|
-
print(k, type(d))
|
|
46
50
|
if isinstance(d, IFDRational):
|
|
47
51
|
d = f"{d.numerator}/{d.denominator}"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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)
|
|
54
74
|
else:
|
|
55
|
-
self.
|
|
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.
|
|
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,5 +1,5 @@
|
|
|
1
1
|
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
-
shinestacker/_version.py,sha256=
|
|
2
|
+
shinestacker/_version.py,sha256=zzuY_qaJa652YnCSIsHTbX-QYu0qr0lMpakTuIQMbtg,21
|
|
3
3
|
shinestacker/algorithms/__init__.py,sha256=1FwVJ3w9GGbFFkjYJRUedTvcdE4j0ieSgaH9RC9iCY4,877
|
|
4
4
|
shinestacker/algorithms/align.py,sha256=840SLh38JePGQv9vgG2H6jHkgHSAYzSpbNDDTxV5ghg,37915
|
|
5
5
|
shinestacker/algorithms/align_auto.py,sha256=DsHuAkFXSHbtFwp6XRaV3Sy1LGcUZWYAFijJXWrd1Bo,3833
|
|
@@ -9,14 +9,14 @@ shinestacker/algorithms/base_stack_algo.py,sha256=mqCCRufLc9k5fZV5Su41AsN1ecHrZJ
|
|
|
9
9
|
shinestacker/algorithms/corrections.py,sha256=DrfLM33D20l4svuuBtoOiH-KGUH_BL1mAV7mHCA_nGA,1094
|
|
10
10
|
shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
|
|
11
11
|
shinestacker/algorithms/depth_map.py,sha256=nRBrZQWbdUqFOtYMEQx9UNdnybrBTeAOr1eV91FlN8U,5611
|
|
12
|
-
shinestacker/algorithms/exif.py,sha256=
|
|
12
|
+
shinestacker/algorithms/exif.py,sha256=jdUl3qMUif3wdQr7z8TnDFUo8iR84Zjmg57nmAmskxc,21729
|
|
13
13
|
shinestacker/algorithms/multilayer.py,sha256=SX4digCMvPxvm9KRrwroUwoAc83ScbmjIjN8s5au3wg,10053
|
|
14
14
|
shinestacker/algorithms/noise_detection.py,sha256=SbWcxSPZIxnThXITAe7koPLKhQZ_gciQby50u3QfkGs,9464
|
|
15
15
|
shinestacker/algorithms/pyramid.py,sha256=Z7tlp8Hh3ploAXJCr0VNe33d8H9GNrlqHXq_LapgRwo,8205
|
|
16
16
|
shinestacker/algorithms/pyramid_auto.py,sha256=fl_jXNYLWsBiX0M0UghzCLqai0SGXlmKYHU7Z9SUYSo,6173
|
|
17
17
|
shinestacker/algorithms/pyramid_tiles.py,sha256=t04_06oYF6QkSSyFQEivHh-GDTska2dQEmfCYoscy-c,12216
|
|
18
18
|
shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
|
|
19
|
-
shinestacker/algorithms/stack.py,sha256=
|
|
19
|
+
shinestacker/algorithms/stack.py,sha256=dRaxNF3Uap18Q6uXWgPMKHSd18Ci0QooEJZciH68_VE,6495
|
|
20
20
|
shinestacker/algorithms/stack_framework.py,sha256=HwB0gDncjJEKHdaR9fFcc2XoRrgxFNrrFDfVyeO4NRM,14616
|
|
21
21
|
shinestacker/algorithms/utils.py,sha256=1RCsOSQ5TSM8y10Wg5JBDWCAEf-vEQReN_5VMtrLW7o,13127
|
|
22
22
|
shinestacker/algorithms/vignetting.py,sha256=Y-K_CTjtNpl0YX86PaM0te-HFxuEcWozhWoB7-g_S7Y,10849
|
|
@@ -26,7 +26,7 @@ shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9y
|
|
|
26
26
|
shinestacker/app/args_parser_opts.py,sha256=G3jQjxBYk87ycmyf8Idk40c5H90O1l0owz0asTodm88,2183
|
|
27
27
|
shinestacker/app/gui_utils.py,sha256=rNDtC6vQ1hAJ5F3Vd-VKglCE06mhleu5eiw-oitgnxU,3656
|
|
28
28
|
shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
|
|
29
|
-
shinestacker/app/main.py,sha256=
|
|
29
|
+
shinestacker/app/main.py,sha256=0dEtkLsshD5xEjK107EyYaM8fJhqFm88taTbf6BMPrk,10671
|
|
30
30
|
shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
|
|
31
31
|
shinestacker/app/project.py,sha256=nwvXllD2FBLQ4ChePQdIGVug46Wh2ubjrJ0sC7klops,2596
|
|
32
32
|
shinestacker/app/retouch.py,sha256=8XcYMv7-feG6yxNCpvlijZQRPlhmRK0OfZO5MuBju-0,2552
|
|
@@ -45,10 +45,10 @@ shinestacker/core/framework.py,sha256=i-_4v--ZtimmlPUs2DmkEVvbsvEDZmbCmOtMVfCxww
|
|
|
45
45
|
shinestacker/core/logging.py,sha256=pN4FGcHwI5ouJKwCVoDWQx_Tg3t84mmPh0xhqszDDkw,3111
|
|
46
46
|
shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
47
|
shinestacker/gui/action_config.py,sha256=OWW32h55OTvM6lbfJc3ZhPoa0vVEvsH63iCbTWo6r6E,25843
|
|
48
|
-
shinestacker/gui/action_config_dialog.py,sha256=
|
|
48
|
+
shinestacker/gui/action_config_dialog.py,sha256=YdVtDKehjIxBYKi7AsFcS8RR-cJT_Xmm6VP8LIlTQ1s,40582
|
|
49
49
|
shinestacker/gui/base_form_dialog.py,sha256=KAUQNtmJazttmOIe4E4pFifbtvcByTAhtCmcIYeA4UE,766
|
|
50
50
|
shinestacker/gui/colors.py,sha256=-HaFprDuzRSKjXoZfX1rdOuvawQAkazqdgLBEiZcFII,1476
|
|
51
|
-
shinestacker/gui/config_dialog.py,sha256=
|
|
51
|
+
shinestacker/gui/config_dialog.py,sha256=vJao8UH8YeE5AuSbYE5Aj9atqo-38DdceHLGsuGvnlw,3544
|
|
52
52
|
shinestacker/gui/flow_layout.py,sha256=3yBU_z7VtvHKpx1H97CHVd81eq9pe1Dcja2EZBGGKcI,3791
|
|
53
53
|
shinestacker/gui/folder_file_selection.py,sha256=CwussPYMguMk8WuyuUKk28VneafwGR-5yiqPo0bp_XE,4158
|
|
54
54
|
shinestacker/gui/gui_images.py,sha256=KxGBFLL2ztfNmvL4pconi3z5HJCoD2HXxpYZP70aUfM,6803
|
|
@@ -89,14 +89,14 @@ shinestacker/retouch/brush_preview.py,sha256=cOFVMCbEsgR_alzmr_-LLghtGU_unrE-hAj
|
|
|
89
89
|
shinestacker/retouch/brush_tool.py,sha256=8uVncTA375uC3Nhp2YM0eZjpOR-nN47i2eGjN8tJzOU,8714
|
|
90
90
|
shinestacker/retouch/denoise_filter.py,sha256=QVXFU54MDcylNWtiIcdQSZ3eClW_xNWZhCMIeoEQ8zk,576
|
|
91
91
|
shinestacker/retouch/display_manager.py,sha256=fTZTGbvmX5DXagexuvbNgOF5GiH2Vv-stLUQQwoglp8,10181
|
|
92
|
-
shinestacker/retouch/exif_data.py,sha256=
|
|
92
|
+
shinestacker/retouch/exif_data.py,sha256=9m2_XwSZk58u3EJQnySLFB-IVdMdyrVWkiLPhcKEfPk,3298
|
|
93
93
|
shinestacker/retouch/file_loader.py,sha256=FTOGOuQRHekofESFDsCvnUU5XnZH_GbLfxXwKnoxZ4s,4832
|
|
94
94
|
shinestacker/retouch/filter_manager.py,sha256=tOGIWj5HjViL1-iXHkd91X-sZ1c1G531pDmLO0x6zx0,866
|
|
95
95
|
shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
|
|
96
|
-
shinestacker/retouch/image_editor_ui.py,sha256=
|
|
96
|
+
shinestacker/retouch/image_editor_ui.py,sha256=a48GiU-Pm6viNe54KEwq6y_Re-YqsB6juxLLj4-C53Y,36189
|
|
97
97
|
shinestacker/retouch/image_view_status.py,sha256=2rWi2ugdyjMhWCtRJkwOnb7-tCtVfnGfCY_54qpZhwM,1970
|
|
98
98
|
shinestacker/retouch/image_viewer.py,sha256=xf1vYZRPb9ClCQbqrqAFhPubdqIIpku7DgcY8O5bvYU,4694
|
|
99
|
-
shinestacker/retouch/io_gui_handler.py,sha256=
|
|
99
|
+
shinestacker/retouch/io_gui_handler.py,sha256=iXVCNIWxLwF28g5H-BePYYzAZgCuksUreITmOO8MI9E,14508
|
|
100
100
|
shinestacker/retouch/io_threads.py,sha256=r0X4it2PfwnmiAU7eStniIfcHhPvuaqdqf5VlnvjZ-4,2832
|
|
101
101
|
shinestacker/retouch/layer_collection.py,sha256=xx8INSLCXIeTQn_nxfCo4QljAmQK1qukSYO1Zk4rqqo,6183
|
|
102
102
|
shinestacker/retouch/overlaid_view.py,sha256=QTTdegUWs99YBZZPlIRdPI5O80U3t_c3HnyegbRqNbA,7029
|
|
@@ -109,9 +109,9 @@ shinestacker/retouch/unsharp_mask_filter.py,sha256=SO-6ZgPPDAO9em_MMefVvvSvt01-2
|
|
|
109
109
|
shinestacker/retouch/view_strategy.py,sha256=jZxB_vX3_0notH0ClxKkLzbdtx4is3vQiYoIP-sDv3M,30216
|
|
110
110
|
shinestacker/retouch/vignetting_filter.py,sha256=M7PZGPdVSq4bqo6wkEznrILMIG3-mTT7iwpgK4Hieyg,3794
|
|
111
111
|
shinestacker/retouch/white_balance_filter.py,sha256=UaH4yxG3fU4vPutBAkV5oTXIQyUTN09x0uTywAzv3sY,8286
|
|
112
|
-
shinestacker-1.9.
|
|
113
|
-
shinestacker-1.9.
|
|
114
|
-
shinestacker-1.9.
|
|
115
|
-
shinestacker-1.9.
|
|
116
|
-
shinestacker-1.9.
|
|
117
|
-
shinestacker-1.9.
|
|
112
|
+
shinestacker-1.9.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
113
|
+
shinestacker-1.9.1.dist-info/METADATA,sha256=Udu0wqbX3XEcA_H38Rc9ZIjFMruYS3vEOTS4kYpKfOY,6883
|
|
114
|
+
shinestacker-1.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
115
|
+
shinestacker-1.9.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
116
|
+
shinestacker-1.9.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
117
|
+
shinestacker-1.9.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|