shinestacker 1.8.1__py3-none-any.whl → 1.9.0__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 +252 -6
- shinestacker/algorithms/multilayer.py +6 -4
- shinestacker/algorithms/stack.py +25 -13
- shinestacker/algorithms/stack_framework.py +2 -2
- shinestacker/algorithms/utils.py +18 -2
- shinestacker/algorithms/vignetting.py +1 -1
- shinestacker/config/constants.py +0 -1
- shinestacker/gui/action_config_dialog.py +2 -1
- shinestacker/gui/folder_file_selection.py +3 -2
- shinestacker/gui/gui_run.py +2 -2
- shinestacker/gui/new_project.py +5 -5
- shinestacker/retouch/exif_data.py +3 -0
- shinestacker/retouch/file_loader.py +3 -3
- shinestacker/retouch/io_gui_handler.py +4 -4
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.0.dist-info}/METADATA +3 -1
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.0.dist-info}/RECORD +21 -21
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.8.1.dist-info → shinestacker-1.9.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.
|
|
1
|
+
__version__ = '1.9.0'
|
shinestacker/algorithms/exif.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0116, W0718, R0911, R0912, E1101
|
|
1
|
+
# pylint: disable=C0114, C0116, W0718, R0911, R0912, E1101, R0915, R1702, R0914, R0917, R0913
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
import io
|
|
5
5
|
import logging
|
|
6
|
+
import traceback
|
|
6
7
|
import cv2
|
|
7
8
|
import numpy as np
|
|
8
9
|
from PIL import Image
|
|
9
10
|
from PIL.TiffImagePlugin import IFDRational
|
|
11
|
+
from PIL.PngImagePlugin import PngInfo
|
|
10
12
|
from PIL.ExifTags import TAGS
|
|
11
13
|
import tifffile
|
|
12
14
|
from .. config.constants import constants
|
|
@@ -58,9 +60,33 @@ def get_exif(exif_filename):
|
|
|
58
60
|
if data is not None:
|
|
59
61
|
exif_data[XMLPACKET] = data
|
|
60
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()
|
|
61
66
|
return image.getexif()
|
|
62
67
|
|
|
63
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
|
+
|
|
64
90
|
def exif_extra_tags_for_tif(exif):
|
|
65
91
|
logger = logging.getLogger(__name__)
|
|
66
92
|
res_x, res_y = exif.get(RESOLUTIONX), exif.get(RESOLUTIONY)
|
|
@@ -153,7 +179,222 @@ def add_exif_data_to_jpg_file(exif, in_filenama, out_filename, verbose=False):
|
|
|
153
179
|
return exif
|
|
154
180
|
|
|
155
181
|
|
|
156
|
-
def
|
|
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'):
|
|
157
398
|
if exif is None:
|
|
158
399
|
write_img(out_filename, image)
|
|
159
400
|
return None
|
|
@@ -168,7 +409,7 @@ def write_image_with_exif_data(exif, image, out_filename, verbose=False):
|
|
|
168
409
|
tifffile.imwrite(out_filename, image, metadata=metadata, compression='adobe_deflate',
|
|
169
410
|
extratags=extra_tags, **exif_tags)
|
|
170
411
|
elif extension_png(out_filename):
|
|
171
|
-
|
|
412
|
+
write_image_with_exif_data_png(exif, image, out_filename, verbose, color_order=color_order)
|
|
172
413
|
return exif
|
|
173
414
|
|
|
174
415
|
|
|
@@ -181,8 +422,10 @@ def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
|
|
|
181
422
|
print_exif(exif)
|
|
182
423
|
if extension_tif(in_filename):
|
|
183
424
|
image_new = tifffile.imread(in_filename)
|
|
184
|
-
|
|
425
|
+
elif extension_jpg(in_filename):
|
|
185
426
|
image_new = Image.open(in_filename)
|
|
427
|
+
elif extension_png(in_filename):
|
|
428
|
+
image_new = cv2.imread(in_filename, cv2.IMREAD_UNCHANGED)
|
|
186
429
|
if extension_jpg(in_filename):
|
|
187
430
|
add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose)
|
|
188
431
|
elif extension_tif(in_filename):
|
|
@@ -191,7 +434,7 @@ def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
|
|
|
191
434
|
tifffile.imwrite(out_filename, image_new, metadata=metadata, compression='adobe_deflate',
|
|
192
435
|
extratags=extra_tags, **exif_tags)
|
|
193
436
|
elif extension_png(in_filename):
|
|
194
|
-
|
|
437
|
+
write_image_with_exif_data_png(exif, image_new, out_filename, verbose)
|
|
195
438
|
return exif
|
|
196
439
|
|
|
197
440
|
|
|
@@ -237,4 +480,7 @@ def print_exif(exif, hide_xml=True):
|
|
|
237
480
|
for tag, (tag_id, data) in exif_data.items():
|
|
238
481
|
if isinstance(data, IFDRational):
|
|
239
482
|
data = f"{data.numerator}/{data.denominator}"
|
|
240
|
-
|
|
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
|
|
146
|
-
|
|
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(
|
|
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)),
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, R0913, R0917
|
|
2
2
|
import os
|
|
3
3
|
import traceback
|
|
4
|
+
import logging
|
|
5
|
+
import numpy as np
|
|
4
6
|
from .. config.constants import constants
|
|
5
7
|
from .. core.framework import TaskBase
|
|
6
8
|
from .. core.colors import color_str
|
|
7
9
|
from .. core.exceptions import InvalidOptionError
|
|
8
|
-
from .utils import write_img,
|
|
10
|
+
from .utils import write_img, extension_supported
|
|
9
11
|
from .stack_framework import ImageSequenceManager, SequentialTask
|
|
10
12
|
from .exif import copy_exif_from_file_to_file
|
|
11
13
|
from .denoise import denoise
|
|
@@ -35,18 +37,28 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
|
|
|
35
37
|
stacked_img = denoise(stacked_img, self.denoise_amount, self.denoise_amount)
|
|
36
38
|
write_img(out_filename, stacked_img)
|
|
37
39
|
if self.exif_path != '':
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
if stacked_img.dtype == np.uint16 and \
|
|
41
|
+
os.path.splitext(out_filename)[-1].lower() == '.png':
|
|
42
|
+
self.sub_message_r(color_str(': exif not supported for 16-bit PNG format',
|
|
43
|
+
constants.LOG_COLOR_WARNING),
|
|
44
|
+
level=logging.WARNING)
|
|
45
|
+
else:
|
|
46
|
+
self.sub_message_r(color_str(': copy exif data', constants.LOG_COLOR_LEVEL_3))
|
|
47
|
+
if not os.path.exists(self.exif_path):
|
|
48
|
+
raise RuntimeError(f"path {self.exif_path} does not exist.")
|
|
49
|
+
try:
|
|
50
|
+
_dirpath, _, fnames = next(os.walk(self.exif_path))
|
|
51
|
+
fnames = [name for name in fnames if extension_supported(name)]
|
|
52
|
+
if len(fnames) == 0:
|
|
53
|
+
raise RuntimeError(f"path {self.exif_path} does not contain image files.")
|
|
54
|
+
exif_filename = os.path.join(self.exif_path, fnames[0])
|
|
55
|
+
copy_exif_from_file_to_file(exif_filename, out_filename)
|
|
56
|
+
self.sub_message_r(' ' * 60)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
traceback.print_tb(e.__traceback__)
|
|
59
|
+
self.sub_message_r(color_str(f': failed to copy EXIF data: {str(e)}',
|
|
60
|
+
constants.LOG_COLOR_WARNING),
|
|
61
|
+
level=logging.WARNING)
|
|
50
62
|
if self.plot_stack:
|
|
51
63
|
idx_str = f"{self.frame_count + 1:04d}" if self.frame_count >= 0 else ''
|
|
52
64
|
name = f"{self.name}: {self.stack_algo.name()}"
|
|
@@ -7,7 +7,7 @@ from .. core.colors import color_str
|
|
|
7
7
|
from .. core.framework import Job, SequentialTask
|
|
8
8
|
from .. core.core_utils import check_path_exists
|
|
9
9
|
from .. core.exceptions import RunStopException
|
|
10
|
-
from .utils import read_img, write_img,
|
|
10
|
+
from .utils import read_img, write_img, extension_supported, get_img_metadata, validate_image
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class StackJob(Job):
|
|
@@ -95,7 +95,7 @@ class ImageSequenceManager:
|
|
|
95
95
|
filelist = []
|
|
96
96
|
for _dirpath, _, filenames in os.walk(d):
|
|
97
97
|
filelist = [os.path.join(_dirpath, name)
|
|
98
|
-
for name in filenames if
|
|
98
|
+
for name in filenames if extension_supported(name)]
|
|
99
99
|
filelist.sort()
|
|
100
100
|
if self.reverse_order:
|
|
101
101
|
filelist.reverse()
|
shinestacker/algorithms/utils.py
CHANGED
|
@@ -18,6 +18,15 @@ EXTENSIONS_TIF = ['tif', 'tiff']
|
|
|
18
18
|
EXTENSIONS_JPG = ['jpg', 'jpeg']
|
|
19
19
|
EXTENSIONS_PNG = ['png']
|
|
20
20
|
EXTENSIONS_PDF = ['pdf']
|
|
21
|
+
EXTENSIONS_SUPPORTED = EXTENSIONS_TIF + EXTENSIONS_JPG + EXTENSIONS_PNG
|
|
22
|
+
EXTENSIONS_GUI_STR = " ".join([f"*.{ext}" for ext in EXTENSIONS_SUPPORTED])
|
|
23
|
+
EXTENSION_GUI_TIF = " ".join([f"*.{ext}" for ext in EXTENSIONS_TIF])
|
|
24
|
+
EXTENSION_GUI_JPG = " ".join([f"*.{ext}" for ext in EXTENSIONS_JPG])
|
|
25
|
+
EXTENSION_GUI_PNG = " ".join([f"*.{ext}" for ext in EXTENSIONS_PNG])
|
|
26
|
+
EXTENSIONS_GUI_SAVE_STR = f"TIFF Files ({EXTENSION_GUI_TIF});;" \
|
|
27
|
+
f"JPEG Files ({EXTENSION_GUI_JPG});;" \
|
|
28
|
+
f"PNG Files ({EXTENSION_GUI_PNG});;" \
|
|
29
|
+
"All Files (*)"
|
|
21
30
|
|
|
22
31
|
|
|
23
32
|
def extension_in(path, exts):
|
|
@@ -56,6 +65,10 @@ def extension_jpg_tif_png(path):
|
|
|
56
65
|
return extension_in(path, EXTENSIONS_JPG + EXTENSIONS_TIF + EXTENSIONS_PNG)
|
|
57
66
|
|
|
58
67
|
|
|
68
|
+
def extension_supported(path):
|
|
69
|
+
return extension_in(path, EXTENSIONS_SUPPORTED)
|
|
70
|
+
|
|
71
|
+
|
|
59
72
|
def read_img(file_path):
|
|
60
73
|
if not os.path.isfile(file_path):
|
|
61
74
|
raise RuntimeError("File does not exist: " + file_path)
|
|
@@ -73,7 +86,10 @@ def write_img(file_path, img):
|
|
|
73
86
|
elif extension_tif(file_path):
|
|
74
87
|
cv2.imwrite(file_path, img, [int(cv2.IMWRITE_TIFF_COMPRESSION), 1])
|
|
75
88
|
elif extension_png(file_path):
|
|
76
|
-
cv2.imwrite(file_path, img
|
|
89
|
+
cv2.imwrite(file_path, img, [
|
|
90
|
+
int(cv2.IMWRITE_PNG_COMPRESSION), 9,
|
|
91
|
+
int(cv2.IMWRITE_PNG_STRATEGY), cv2.IMWRITE_PNG_STRATEGY_HUFFMAN_ONLY
|
|
92
|
+
])
|
|
77
93
|
|
|
78
94
|
|
|
79
95
|
def img_8bit(img):
|
|
@@ -96,7 +112,7 @@ def img_bw(img):
|
|
|
96
112
|
def get_first_image_file(filenames):
|
|
97
113
|
first_img_file = None
|
|
98
114
|
for filename in filenames:
|
|
99
|
-
if os.path.isfile(filename) and
|
|
115
|
+
if os.path.isfile(filename) and extension_supported(filename):
|
|
100
116
|
first_img_file = filename
|
|
101
117
|
break
|
|
102
118
|
if first_img_file is None:
|
|
@@ -185,7 +185,7 @@ class Vignetting(SubAction):
|
|
|
185
185
|
for i, p in enumerate(self.percentiles):
|
|
186
186
|
s1 = sigmoid_model(0, *params) / self.v0
|
|
187
187
|
s2 = sigmoid_model(self.r_max, *params) / self.v0
|
|
188
|
-
if s1 > p
|
|
188
|
+
if s1 > p > s2:
|
|
189
189
|
try:
|
|
190
190
|
c = bisect(lambda x: sigmoid_model(x, *params) / self.v0 - p, 0, self.r_max)
|
|
191
191
|
except Exception as e:
|
shinestacker/config/constants.py
CHANGED
|
@@ -6,6 +6,7 @@ from PySide6.QtCore import QTimer
|
|
|
6
6
|
from PySide6.QtWidgets import QWidget, QLabel, QMessageBox, QStackedWidget
|
|
7
7
|
from .. config.constants import constants
|
|
8
8
|
from .. config.app_config import AppConfig
|
|
9
|
+
from .. algorithms.utils import EXTENSIONS_SUPPORTED
|
|
9
10
|
from .. algorithms.align import validate_align_config
|
|
10
11
|
from . action_config import (
|
|
11
12
|
DefaultActionConfigurator, add_tab, create_tab_layout, create_tab_widget,
|
|
@@ -122,7 +123,7 @@ class JobConfigurator(DefaultActionConfigurator):
|
|
|
122
123
|
return 0
|
|
123
124
|
count = 0
|
|
124
125
|
for filename in os.listdir(path):
|
|
125
|
-
if filename.lower()
|
|
126
|
+
if os.path.splitext(filename)[-1][1:].lower() in EXTENSIONS_SUPPORTED:
|
|
126
127
|
count += 1
|
|
127
128
|
return count
|
|
128
129
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
2
|
import os
|
|
3
|
+
from PySide6.QtCore import Qt
|
|
3
4
|
from PySide6.QtWidgets import (QWidget, QRadioButton, QButtonGroup, QLineEdit,
|
|
4
5
|
QPushButton, QHBoxLayout, QVBoxLayout, QFileDialog, QMessageBox)
|
|
5
|
-
from
|
|
6
|
+
from .. algorithms.utils import EXTENSIONS_GUI_STR
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class FolderFileSelectionWidget(QWidget):
|
|
@@ -73,7 +74,7 @@ class FolderFileSelectionWidget(QWidget):
|
|
|
73
74
|
def browse_files(self):
|
|
74
75
|
files, _ = QFileDialog.getOpenFileNames(
|
|
75
76
|
self, "Select Input Files", "",
|
|
76
|
-
"Image files (
|
|
77
|
+
f"Image files ({EXTENSIONS_GUI_STR})"
|
|
77
78
|
)
|
|
78
79
|
if files:
|
|
79
80
|
parent_dir = os.path.dirname(files[0])
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -9,7 +9,7 @@ from PySide6.QtCore import Signal, Slot
|
|
|
9
9
|
from .. config.constants import constants
|
|
10
10
|
from .. config.gui_constants import gui_constants
|
|
11
11
|
from .colors import RED_BUTTON_STYLE, BLUE_BUTTON_STYLE, BLUE_COMBO_STYLE
|
|
12
|
-
from .. algorithms.utils import
|
|
12
|
+
from .. algorithms.utils import extension_supported, extension_pdf
|
|
13
13
|
from .gui_logging import LogWorker, QTextEditLogger
|
|
14
14
|
from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
|
|
15
15
|
from .colors import (
|
|
@@ -209,7 +209,7 @@ class RunWindow(QTextEditLogger):
|
|
|
209
209
|
try:
|
|
210
210
|
if extension_pdf(path):
|
|
211
211
|
image_view = GuiPdfView(path, self)
|
|
212
|
-
elif
|
|
212
|
+
elif extension_supported(path):
|
|
213
213
|
image_view = GuiImageView(path, self)
|
|
214
214
|
else:
|
|
215
215
|
raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -8,7 +8,7 @@ from PySide6.QtCore import Qt
|
|
|
8
8
|
from .. config.gui_constants import gui_constants
|
|
9
9
|
from .. config.constants import constants
|
|
10
10
|
from .. config.app_config import AppConfig
|
|
11
|
-
from .. algorithms.utils import read_img,
|
|
11
|
+
from .. algorithms.utils import read_img, extension_supported
|
|
12
12
|
from .. algorithms.stack import get_bunches
|
|
13
13
|
from .folder_file_selection import FolderFileSelectionWidget
|
|
14
14
|
from .base_form_dialog import BaseFormDialog
|
|
@@ -208,7 +208,7 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
208
208
|
return 0
|
|
209
209
|
count = 0
|
|
210
210
|
for filename in os.listdir(path):
|
|
211
|
-
if
|
|
211
|
+
if extension_supported(filename):
|
|
212
212
|
count += 1
|
|
213
213
|
return count
|
|
214
214
|
if self.input_widget.get_selection_mode() == 'files' and \
|
|
@@ -273,7 +273,7 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
273
273
|
file_path = None
|
|
274
274
|
for filename in files:
|
|
275
275
|
full_path = os.path.join(path, filename)
|
|
276
|
-
if
|
|
276
|
+
if extension_supported(full_path):
|
|
277
277
|
file_path = full_path
|
|
278
278
|
break
|
|
279
279
|
if file_path is None:
|
|
@@ -284,8 +284,8 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
284
284
|
height, width = img.shape[:2]
|
|
285
285
|
n_bytes = 1 if img.dtype == np.uint8 else 2
|
|
286
286
|
n_bits = 8 if img.dtype == np.uint8 else 16
|
|
287
|
-
n_gbytes =
|
|
288
|
-
if n_gbytes >
|
|
287
|
+
n_gbytes = 3.0 * n_bytes * height * width * self.n_image_files / constants.ONE_GIGA
|
|
288
|
+
if n_gbytes > 4 and not self.bunch_stack.isChecked():
|
|
289
289
|
msg = QMessageBox()
|
|
290
290
|
msg.setStyleSheet("""
|
|
291
291
|
QMessageBox {
|
|
@@ -42,8 +42,11 @@ class ExifData(BaseFormDialog):
|
|
|
42
42
|
data = exif_dict(self.exif)
|
|
43
43
|
if len(data) > 0:
|
|
44
44
|
for k, (_, d) in data.items():
|
|
45
|
+
print(k, type(d))
|
|
45
46
|
if isinstance(d, IFDRational):
|
|
46
47
|
d = f"{d.numerator}/{d.denominator}"
|
|
48
|
+
elif len(str(d)) > 40:
|
|
49
|
+
d = f"{str(d):.40}..."
|
|
47
50
|
else:
|
|
48
51
|
d = f"{d}"
|
|
49
52
|
if "<<<" not in d and k != 'IPTCNAA':
|
|
@@ -5,7 +5,7 @@ import numpy as np
|
|
|
5
5
|
import cv2
|
|
6
6
|
from psdtags import PsdChannelId
|
|
7
7
|
from PySide6.QtCore import QThread, Signal
|
|
8
|
-
from .. algorithms.utils import read_img, extension_tif, extension_jpg
|
|
8
|
+
from .. algorithms.utils import read_img, extension_tif, extension_jpg, extension_png
|
|
9
9
|
from .. algorithms.multilayer import read_multilayer_tiff
|
|
10
10
|
|
|
11
11
|
|
|
@@ -50,10 +50,10 @@ class FileLoader(QThread):
|
|
|
50
50
|
raise RuntimeError(f"Path {path} does not exist.")
|
|
51
51
|
if not os.path.isfile(path):
|
|
52
52
|
raise RuntimeError(f"Path {path} is not a file.")
|
|
53
|
-
if extension_jpg(path):
|
|
53
|
+
if extension_jpg(path) or extension_png(path):
|
|
54
54
|
try:
|
|
55
55
|
stack = np.array([cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)])
|
|
56
|
-
return stack, [path.
|
|
56
|
+
return stack, [os.path.splitext(os.path.basename(path))[0]]
|
|
57
57
|
except Exception as e:
|
|
58
58
|
traceback.print_tb(e.__traceback__)
|
|
59
59
|
return None, None
|
|
@@ -7,6 +7,7 @@ from PySide6.QtWidgets import (QFileDialog, QMessageBox, QVBoxLayout, QLabel, QD
|
|
|
7
7
|
QApplication, QProgressBar)
|
|
8
8
|
from PySide6.QtGui import QGuiApplication, QCursor
|
|
9
9
|
from PySide6.QtCore import Qt, QObject, QTimer, Signal
|
|
10
|
+
from .. algorithms.utils import EXTENSIONS_GUI_STR, EXTENSIONS_GUI_SAVE_STR
|
|
10
11
|
from .. algorithms.exif import get_exif, write_image_with_exif_data
|
|
11
12
|
from .file_loader import FileLoader
|
|
12
13
|
from .exif_data import ExifData
|
|
@@ -135,7 +136,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
135
136
|
if file_paths is None:
|
|
136
137
|
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
137
138
|
self.parent(), "Open Image", "",
|
|
138
|
-
"Images (
|
|
139
|
+
F"Images ({EXTENSIONS_GUI_STR});;All Files (*)")
|
|
139
140
|
if not file_paths:
|
|
140
141
|
return
|
|
141
142
|
if self.loader_thread and self.loader_thread.isRunning():
|
|
@@ -167,7 +168,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
167
168
|
def import_frames(self):
|
|
168
169
|
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
169
170
|
self.parent(), "Select frames", "",
|
|
170
|
-
"Images Images (
|
|
171
|
+
f"Images Images ({EXTENSIONS_GUI_STR});;All Files (*)")
|
|
171
172
|
if file_paths:
|
|
172
173
|
self.import_frames_from_files(file_paths)
|
|
173
174
|
|
|
@@ -286,8 +287,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
286
287
|
if self.layer_stack() is None:
|
|
287
288
|
return
|
|
288
289
|
path, _ = QFileDialog.getSaveFileName(
|
|
289
|
-
self.parent(), "Save Image", "",
|
|
290
|
-
"TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
|
|
290
|
+
self.parent(), "Save Image", "", EXTENSIONS_GUI_SAVE_STR)
|
|
291
291
|
if path:
|
|
292
292
|
self.save_master_to_path(path)
|
|
293
293
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 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.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
-
shinestacker/_version.py,sha256=
|
|
2
|
+
shinestacker/_version.py,sha256=WddBug1JDIVoyG0cVXLv1jzZiNT54lgulb9KP0Q3AnU,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,17 +9,17 @@ 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=
|
|
13
|
-
shinestacker/algorithms/multilayer.py,sha256=
|
|
12
|
+
shinestacker/algorithms/exif.py,sha256=0kYxk_cZeBgmiB2vEkmKNBVbCNeq_4KF-8lub9ehSPQ,19934
|
|
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=
|
|
20
|
-
shinestacker/algorithms/stack_framework.py,sha256=
|
|
21
|
-
shinestacker/algorithms/utils.py,sha256=
|
|
22
|
-
shinestacker/algorithms/vignetting.py,sha256=
|
|
19
|
+
shinestacker/algorithms/stack.py,sha256=OkKC9LWTTPSIFy4teiqC4TFjgRUGp4eiSGlxLKdmdL0,6488
|
|
20
|
+
shinestacker/algorithms/stack_framework.py,sha256=HwB0gDncjJEKHdaR9fFcc2XoRrgxFNrrFDfVyeO4NRM,14616
|
|
21
|
+
shinestacker/algorithms/utils.py,sha256=1RCsOSQ5TSM8y10Wg5JBDWCAEf-vEQReN_5VMtrLW7o,13127
|
|
22
|
+
shinestacker/algorithms/vignetting.py,sha256=Y-K_CTjtNpl0YX86PaM0te-HFxuEcWozhWoB7-g_S7Y,10849
|
|
23
23
|
shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
|
|
24
24
|
shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
|
|
@@ -34,7 +34,7 @@ shinestacker/app/settings_dialog.py,sha256=x4-mYEUcB1I9SoQmzDpxFzfLI5JU0hbeqmIyd
|
|
|
34
34
|
shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
|
|
35
35
|
shinestacker/config/app_config.py,sha256=rM1Rndk1GDa5c0AhcVNEN9zSAzxPZixzQYfjODbJUwE,771
|
|
36
36
|
shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
|
|
37
|
-
shinestacker/config/constants.py,sha256=
|
|
37
|
+
shinestacker/config/constants.py,sha256=qpQ7uuf7qnFesiq4zvt6A7ASjLbyADbbeMzkW-GCbe4,8470
|
|
38
38
|
shinestacker/config/gui_constants.py,sha256=PNxzwmVEppJ2mV_vwp68NhWzJOEitVy1Pk9SwSmRsho,2882
|
|
39
39
|
shinestacker/config/settings.py,sha256=jdRMJRT6AzO-dnvmOCwEGURsGBt36ILH-xszNIvE0ew,4845
|
|
40
40
|
shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
|
|
@@ -45,18 +45,18 @@ 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=8vZ1IxC953JXadhb27z3-GiNUZ8G_lg6MVnreuDOw_A,40777
|
|
49
49
|
shinestacker/gui/base_form_dialog.py,sha256=KAUQNtmJazttmOIe4E4pFifbtvcByTAhtCmcIYeA4UE,766
|
|
50
50
|
shinestacker/gui/colors.py,sha256=-HaFprDuzRSKjXoZfX1rdOuvawQAkazqdgLBEiZcFII,1476
|
|
51
51
|
shinestacker/gui/config_dialog.py,sha256=yt3nvh0HPHQuCn3AFlzlIHUJnnxcz-Rrw3W3jS9ZYiE,3447
|
|
52
52
|
shinestacker/gui/flow_layout.py,sha256=3yBU_z7VtvHKpx1H97CHVd81eq9pe1Dcja2EZBGGKcI,3791
|
|
53
|
-
shinestacker/gui/folder_file_selection.py,sha256=
|
|
53
|
+
shinestacker/gui/folder_file_selection.py,sha256=CwussPYMguMk8WuyuUKk28VneafwGR-5yiqPo0bp_XE,4158
|
|
54
54
|
shinestacker/gui/gui_images.py,sha256=KxGBFLL2ztfNmvL4pconi3z5HJCoD2HXxpYZP70aUfM,6803
|
|
55
55
|
shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
|
|
56
|
-
shinestacker/gui/gui_run.py,sha256=
|
|
56
|
+
shinestacker/gui/gui_run.py,sha256=Tp3BQTbASdfyELQonJPM10dX9mWb7TdecsIjzCnVQsA,15680
|
|
57
57
|
shinestacker/gui/main_window.py,sha256=VYGX-w-A8sy1zsQAJEfLpImax8oB-inx_nZ2XofDEBQ,25777
|
|
58
58
|
shinestacker/gui/menu_manager.py,sha256=mS-pRMymd1yYimbr6Z5YXjMA5AsNuaNcezs8MYWF2DU,12364
|
|
59
|
-
shinestacker/gui/new_project.py,sha256=
|
|
59
|
+
shinestacker/gui/new_project.py,sha256=fnTWxT0YS390T4CTu6Cdl7pWrjsCiphnKZJvDLzXGlE,16728
|
|
60
60
|
shinestacker/gui/project_controller.py,sha256=h2x7Z1MFKXQGB4dGmdLcXQgcDTtId9RMi3m-4pSli2Y,16963
|
|
61
61
|
shinestacker/gui/project_converter.py,sha256=Gmna0HwbvACcXiX74TaQYumif8ZV8sZ2APLTMM-L1mU,7436
|
|
62
62
|
shinestacker/gui/project_editor.py,sha256=9KEH-CkIbK_yLKRo184C08uYXQ9_aqepEGQrKRqhfUg,25991
|
|
@@ -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=
|
|
93
|
-
shinestacker/retouch/file_loader.py,sha256=
|
|
92
|
+
shinestacker/retouch/exif_data.py,sha256=17_f2MK1PPfwD1PKAiH5t2l7xp6MWrvfeK5buJECMA0,2012
|
|
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
96
|
shinestacker/retouch/image_editor_ui.py,sha256=w6tyeYm1Arjyr-MxbLNKYvURV0qEZqigK0iUoqGy92o,34244
|
|
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=ADswLdpkrSyQW6GfHgfUosaKDd9_li5xilTNMRA7bsE,14626
|
|
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.
|
|
113
|
-
shinestacker-1.
|
|
114
|
-
shinestacker-1.
|
|
115
|
-
shinestacker-1.
|
|
116
|
-
shinestacker-1.
|
|
117
|
-
shinestacker-1.
|
|
112
|
+
shinestacker-1.9.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
113
|
+
shinestacker-1.9.0.dist-info/METADATA,sha256=ekT-bHQmMgyxKc8igb3YntWBacm_Zr_Uyi3lve-QuQU,6883
|
|
114
|
+
shinestacker-1.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
115
|
+
shinestacker-1.9.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
116
|
+
shinestacker-1.9.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
117
|
+
shinestacker-1.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|