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 CHANGED
@@ -1 +1 @@
1
- __version__ = '1.8.1'
1
+ __version__ = '1.9.0'
@@ -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 write_image_with_exif_data(exif, image, out_filename, verbose=False):
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
- image.save(out_filename, 'PNG', exif=exif, quality=100)
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
- else:
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
- image_new.save(out_filename, 'PNG', exif=exif, quality=100)
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
- logger.info(msg=f"{tag:25} [#{tag_id:5d}]: {data}")
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)),
@@ -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, extension_tif_jpg
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
- self.sub_message_r(color_str(': copy exif data', constants.LOG_COLOR_LEVEL_3))
39
- if not os.path.exists(self.exif_path):
40
- raise RuntimeError(f"Path {self.exif_path} does not exist.")
41
- try:
42
- _dirpath, _, fnames = next(os.walk(self.exif_path))
43
- fnames = [name for name in fnames if extension_tif_jpg(name)]
44
- exif_filename = os.path.join(self.exif_path, fnames[0])
45
- copy_exif_from_file_to_file(exif_filename, out_filename)
46
- self.sub_message_r(' ' * 60)
47
- except Exception as e:
48
- traceback.print_tb(e.__traceback__)
49
- raise RuntimeError("Can't copy EXIF data") from e
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, extension_tif_jpg, get_img_metadata, validate_image
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 extension_tif_jpg(name)]
98
+ for name in filenames if extension_supported(name)]
99
99
  filelist.sort()
100
100
  if self.reverse_order:
101
101
  filelist.reverse()
@@ -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 extension_tif_jpg(filename):
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 and s2 < 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:
@@ -7,7 +7,6 @@ import os
7
7
  class _Constants:
8
8
  APP_TITLE = "Shine Stacker"
9
9
  APP_STRING = "ShineStacker"
10
- EXTENSIONS = set(["jpeg", "jpg", "png", "tif", "tiff"])
11
10
 
12
11
  NUM_UINT8 = 256
13
12
  NUM_UINT16 = 65536
@@ -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().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff')):
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 PySide6.QtCore import Qt
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 (*.png *.jpg *.jpeg *.tif *.tiff)"
77
+ f"Image files ({EXTENSIONS_GUI_STR})"
77
78
  )
78
79
  if files:
79
80
  parent_dir = os.path.dirname(files[0])
@@ -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 extension_jpg_tif_png, extension_pdf
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 extension_jpg_tif_png(path):
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]}.")
@@ -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, extension_tif_jpg
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 extension_tif_jpg(filename):
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 extension_tif_jpg(full_path):
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 = float(n_bytes * height * width * self.n_image_files) / constants.ONE_GIGA
288
- if n_gbytes > 1 and not self.bunch_stack.isChecked():
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.split('/')[-1].split('.')[0]]
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 (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
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 (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
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.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.
@@ -1,5 +1,5 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=VeCo3jG4eMq5JgUgBBkmfX0RYN_MfWUdUGiQ16h2kPg,21
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=SM4ZDDe8hCJ3xY6053FNndOiwzEStzdp0WrXurlcHVc,9429
13
- shinestacker/algorithms/multilayer.py,sha256=EEMDr2NlCU9DCFO5ykbBrY-2q9oBUD0-ctm7x0IXU5U,9911
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=VvYo6w01sGbMIWS3w_4fVz6k7qOTpuEREXTP4kze6B0,5738
20
- shinestacker/algorithms/stack_framework.py,sha256=YEuaxDqs3gytZMUnub69gcuv5CnbwU3cLxxPQj6HsXQ,14612
21
- shinestacker/algorithms/utils.py,sha256=VJFOT-OBtDe5ds64VgwyZKa8AX1N91SSJlsgUVC9vMs,12303
22
- shinestacker/algorithms/vignetting.py,sha256=KFLPbBv8EAKB5PpO4QIqMbTLEufg-lvxOGkVkiOSEqI,10855
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=fdZ8qp4S5KM_MfXnLCOTnHemNQMcHEio-VatGkDH9-E,8530
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=GYOnTXoEjHHWMS5RECaqtkMMI37OjkP6W5WXPKjYjwE,40727
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=IYWfZQFkoD5iO7zJ7BxVVDP9F3Dc0EXLILAhL4q-Cb8,4117
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=38ke2Zq7KfQBZDNCzfw6RVIUdDTElLKf-tawBarlWyw,15684
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=gvFNToDqLxWbgGBJ19u_2Tn_DlfXTVWQ5wE0dHErXL0,16723
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=LF-fRXW-reMq-xJ_QRE5j8DC2LVGKIlC6MR3QbC1cdg,1896
93
- shinestacker/retouch/file_loader.py,sha256=z02-A8_uDZxayI1NFTxT2GVUvEBWStchX9hlN1o5-0U,4784
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=tyHMmR6_uE_IEI-CIcJibVhupxarKYHzX2fuhnesHaI,14616
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.8.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
113
- shinestacker-1.8.1.dist-info/METADATA,sha256=3Lq1O-DEp4lKM1EcwARqdZ6T92cjXw5TXerHxIC30Ws,6881
114
- shinestacker-1.8.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
- shinestacker-1.8.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
116
- shinestacker-1.8.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
117
- shinestacker-1.8.1.dist-info/RECORD,,
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,,