shinestacker 1.8.1__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 CHANGED
@@ -1 +1 @@
1
- __version__ = '1.8.1'
1
+ __version__ = '1.9.1'
@@ -1,12 +1,12 @@
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
- import re
4
- import io
5
3
  import logging
4
+ import traceback
6
5
  import cv2
7
6
  import numpy as np
8
7
  from PIL import Image
9
8
  from PIL.TiffImagePlugin import IFDRational
9
+ from PIL.PngImagePlugin import PngInfo
10
10
  from PIL.ExifTags import TAGS
11
11
  import tifffile
12
12
  from .. config.constants import constants
@@ -35,14 +35,17 @@ NO_COPY_TIFF_TAGS = ["Compression", "StripOffsets", "RowsPerStrip", "StripByteCo
35
35
 
36
36
 
37
37
  def extract_enclosed_data_for_jpg(data, head, foot):
38
- size = len(foot.decode('ascii'))
39
- xmp_start, xmp_end = data.find(head), data.find(foot)
40
- if xmp_start != -1 and xmp_end != -1:
41
- return re.sub(
42
- b'[^\x20-\x7E]', b'',
43
- data[xmp_start:xmp_end + size]
44
- ).decode().replace('\x00', '').encode()
45
- return None
38
+ try:
39
+ xmp_start = data.find(head)
40
+ if xmp_start == -1:
41
+ return None
42
+ xmp_end = data.find(foot, xmp_start)
43
+ if xmp_end == -1:
44
+ return None
45
+ xmp_end += len(foot)
46
+ return data[xmp_start:xmp_end]
47
+ except Exception:
48
+ return None
46
49
 
47
50
 
48
51
  def get_exif(exif_filename):
@@ -58,9 +61,48 @@ def get_exif(exif_filename):
58
61
  if data is not None:
59
62
  exif_data[XMLPACKET] = data
60
63
  return exif_data
64
+ if extension_png(exif_filename):
65
+ exif_data = get_exif_from_png(image)
66
+ return exif_data if exif_data else image.getexif()
61
67
  return image.getexif()
62
68
 
63
69
 
70
+ def get_exif_from_png(image):
71
+ exif_data = {}
72
+ try:
73
+ exif_from_image = image.getexif()
74
+ if exif_from_image:
75
+ exif_data.update(dict(exif_from_image))
76
+ except Exception:
77
+ pass
78
+ try:
79
+ if hasattr(image, 'text') and image.text:
80
+ for key, value in image.text.items():
81
+ exif_data[f"PNG_{key}"] = value
82
+ if hasattr(image, 'info') and image.info:
83
+ for key, value in image.info.items():
84
+ if key not in ['dpi', 'gamma']:
85
+ exif_data[f"PNG_{key}"] = value
86
+ except Exception:
87
+ pass
88
+ return exif_data
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
+
64
106
  def exif_extra_tags_for_tif(exif):
65
107
  logger = logging.getLogger(__name__)
66
108
  res_x, res_y = exif.get(RESOLUTIONX), exif.get(RESOLUTIONY)
@@ -81,8 +123,13 @@ def exif_extra_tags_for_tif(exif):
81
123
  try:
82
124
  if tag_id not in (IMAGERESOURCES, INTERCOLORPROFILE):
83
125
  if tag_id == XMLPACKET:
84
- data = re.sub(b'[^\x20-\x7E]', b'', data)
85
- data = data.decode()
126
+ try:
127
+ decoded = data.decode('utf-8')
128
+ data = decoded.encode('utf-8')
129
+ except UnicodeDecodeError:
130
+ logger.debug("XMLPACKET contains non-UTF8 data, preserving as bytes")
131
+ else:
132
+ data = safe_decode_bytes(data)
86
133
  except Exception:
87
134
  logger.warning(msg=f"Copy: can't decode EXIF tag {tag:25} [#{tag_id}]")
88
135
  data = '<<< decode error >>>'
@@ -125,50 +172,309 @@ def get_tiff_dtype_count(value):
125
172
  return 2, len(str(value)) + 1 # Default for othre cases (ASCII string)
126
173
 
127
174
 
128
- def add_exif_data_to_jpg_file(exif, in_filenama, out_filename, verbose=False):
175
+ def add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose=False):
129
176
  logger = logging.getLogger(__name__)
130
177
  if exif is None:
131
178
  raise RuntimeError('No exif data provided.')
132
179
  if verbose:
133
180
  print_exif(exif)
134
- xmp_data = extract_enclosed_data_for_jpg(exif[XMLPACKET], b'<x:xmpmeta', b'</x:xmpmeta>')
135
- with Image.open(in_filenama) as image:
136
- with io.BytesIO() as buffer:
137
- image.save(buffer, format="JPEG", exif=exif.tobytes(), quality=100)
138
- jpeg_data = buffer.getvalue()
139
- if xmp_data is not None:
140
- app1_marker_pos = jpeg_data.find(b'\xFF\xE1')
141
- if app1_marker_pos == -1:
142
- app1_marker_pos = len(jpeg_data) - 2
143
- updated_data = (
144
- jpeg_data[:app1_marker_pos] +
145
- b'\xFF\xE1' + len(xmp_data).to_bytes(2, 'big') +
146
- xmp_data + jpeg_data[app1_marker_pos:]
147
- )
148
- else:
149
- logger.warning("Copy: can't find XMLPacket in JPG EXIF data")
150
- updated_data = jpeg_data
151
- with open(out_filename, 'wb') as f:
152
- f.write(updated_data)
153
- return exif
181
+ xmp_data = None
182
+ if XMLPACKET in exif:
183
+ xmp_data = exif[XMLPACKET]
184
+ if isinstance(xmp_data, bytes):
185
+ xmp_start = xmp_data.find(b'<x:xmpmeta')
186
+ xmp_end = xmp_data.find(b'</x:xmpmeta>')
187
+ if xmp_start != -1 and xmp_end != -1:
188
+ xmp_end += len(b'</x:xmpmeta>')
189
+ xmp_data = xmp_data[xmp_start:xmp_end]
190
+ with Image.open(in_filename) as image:
191
+ if hasattr(exif, 'tobytes'):
192
+ exif_bytes = exif.tobytes()
193
+ else:
194
+ exif_bytes = exif
195
+ image.save(out_filename, "JPEG", exif=exif_bytes, quality=100)
196
+ if xmp_data and isinstance(xmp_data, bytes):
197
+ try:
198
+ _insert_xmp_into_jpeg(out_filename, xmp_data, verbose)
199
+ except Exception as e:
200
+ if verbose:
201
+ logger.warning(msg=f"Failed to insert XMP data: {e}")
202
+
203
+
204
+ def _insert_xmp_into_jpeg(jpeg_path, xmp_data, verbose=False):
205
+ logger = logging.getLogger(__name__)
206
+ with open(jpeg_path, 'rb') as f:
207
+ jpeg_data = f.read()
208
+ soi_pos = jpeg_data.find(b'\xFF\xD8')
209
+ if soi_pos == -1:
210
+ if verbose:
211
+ logger.warning("No SOI marker found, cannot insert XMP")
212
+ return
213
+ insert_pos = soi_pos + 2
214
+ current_pos = insert_pos
215
+ while current_pos < len(jpeg_data) - 4:
216
+ if jpeg_data[current_pos] != 0xFF:
217
+ break
218
+ marker = jpeg_data[current_pos + 1]
219
+ if marker == 0xDA:
220
+ break
221
+ segment_length = int.from_bytes(jpeg_data[current_pos + 2:current_pos + 4], 'big')
222
+ if marker == 0xE1:
223
+ insert_pos = current_pos + 2 + segment_length
224
+ current_pos = insert_pos
225
+ continue
226
+ current_pos += 2 + segment_length
227
+ xmp_identifier = b'http://ns.adobe.com/xap/1.0/\x00'
228
+ xmp_payload = xmp_identifier + xmp_data
229
+ segment_length = len(xmp_payload) + 2
230
+ xmp_segment = b'\xFF\xE1' + segment_length.to_bytes(2, 'big') + xmp_payload
231
+ updated_data = (
232
+ jpeg_data[:insert_pos] +
233
+ xmp_segment +
234
+ jpeg_data[insert_pos:]
235
+ )
236
+ with open(jpeg_path, 'wb') as f:
237
+ f.write(updated_data)
238
+ if verbose:
239
+ logger.info("Successfully inserted XMP data into JPEG")
240
+
241
+
242
+ def create_xmp_from_exif(exif_data):
243
+ xmp_elements = []
244
+ if exif_data:
245
+ for tag_id, value in exif_data.items():
246
+ if isinstance(tag_id, int):
247
+ if tag_id == 270 and value: # ImageDescription
248
+ desc = value
249
+ if isinstance(desc, bytes):
250
+ desc = desc.decode('utf-8', errors='ignore')
251
+ xmp_elements.append(
252
+ f'<dc:description><rdf:Alt><rdf:li xml:lang="x-default">{desc}</rdf:li>'
253
+ '</rdf:Alt></dc:description>')
254
+ elif tag_id == 315 and value: # Artist
255
+ artist = value
256
+ if isinstance(artist, bytes):
257
+ artist = artist.decode('utf-8', errors='ignore')
258
+ xmp_elements.append(
259
+ f'<dc:creator><rdf:Seq><rdf:li>{artist}</rdf:li>'
260
+ '</rdf:Seq></dc:creator>')
261
+ elif tag_id == 33432 and value: # Copyright
262
+ copyright_tag = value
263
+ if isinstance(copyright_tag, bytes):
264
+ copyright_tag = copyright_tag.decode('utf-8', errors='ignore')
265
+ xmp_elements.append(
266
+ f'<dc:rights><rdf:Alt><rdf:li xml:lang="x-default">{copyright_tag}</rdf:li>'
267
+ '</rdf:Alt></dc:rights>')
268
+ elif tag_id == 271 and value: # Make
269
+ make = value
270
+ if isinstance(make, bytes):
271
+ make = make.decode('utf-8', errors='ignore')
272
+ xmp_elements.append(f'<tiff:Make>{make}</tiff:Make>')
273
+ elif tag_id == 272 and value: # Model
274
+ model = value
275
+ if isinstance(model, bytes):
276
+ model = model.decode('utf-8', errors='ignore')
277
+ xmp_elements.append(f'<tiff:Model>{model}</tiff:Model>')
278
+ elif tag_id == 306 and value: # DateTime
279
+ datetime_val = value
280
+ if isinstance(datetime_val, bytes):
281
+ datetime_val = datetime_val.decode('utf-8', errors='ignore')
282
+ if ':' in datetime_val:
283
+ datetime_val = datetime_val.replace(':', '-', 2).replace(' ', 'T')
284
+ xmp_elements.append(f'<xmp:CreateDate>{datetime_val}</xmp:CreateDate>')
285
+ elif tag_id == 305 and value: # Software
286
+ software = value
287
+ if isinstance(software, bytes):
288
+ software = software.decode('utf-8', errors='ignore')
289
+ xmp_elements.append(f'<xmp:CreatorTool>{software}</xmp:CreatorTool>')
290
+ if xmp_elements:
291
+ xmp_content = '\n '.join(xmp_elements)
292
+ xmp_template = f"""<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
293
+ <x:xmpmeta xmlns:x='adobe:ns:meta/'
294
+ x:xmptk='Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21'>
295
+ <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
296
+ <rdf:Description rdf:about=''
297
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
298
+ xmlns:xmp='http://ns.adobe.com/xap/1.0/'
299
+ xmlns:tiff='http://ns.adobe.com/tiff/1.0/'
300
+ xmlns:exif='http://ns.adobe.com/exif/1.0/'>
301
+ {xmp_content}
302
+ </rdf:Description>
303
+ </rdf:RDF>
304
+ </x:xmpmeta>
305
+ <?xpacket end='w'?>"""
306
+ return xmp_template
307
+ return """<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
308
+ <x:xmpmeta xmlns:x='adobe:ns:meta/'
309
+ x:xmptk='Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21'>
310
+ <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
311
+ <rdf:Description rdf:about=''/>
312
+ </rdf:RDF>
313
+ </x:xmpmeta>
314
+ <?xpacket end='w'?>"""
315
+
316
+
317
+ def write_image_with_exif_data_png(exif, image, out_filename, verbose=False, color_order='auto'):
318
+ logger = logging.getLogger(__name__)
319
+ if isinstance(image, np.ndarray) and image.dtype == np.uint16:
320
+ if verbose:
321
+ logger.warning(msg="EXIF data not supported for 16-bit PNG format")
322
+ write_img(out_filename, image)
323
+ return
324
+ pil_image = _convert_to_pil_image(image, color_order, verbose, logger)
325
+ pnginfo, icc_profile = _prepare_png_metadata(exif, verbose, logger)
326
+ try:
327
+ save_args = {'format': 'PNG', 'pnginfo': pnginfo}
328
+ if icc_profile:
329
+ save_args['icc_profile'] = icc_profile
330
+ if verbose:
331
+ logger.info(msg="Saved PNG with ICC profile and metadata")
332
+ else:
333
+ if verbose:
334
+ logger.info(msg="Saved PNG without ICC profile but with metadata")
335
+ pil_image.save(out_filename, **save_args)
336
+ if verbose:
337
+ logger.info(msg=f"Successfully wrote PNG with metadata: {out_filename}")
338
+ except Exception as e:
339
+ if verbose:
340
+ logger.error(msg=f"Failed to write PNG with metadata: {e}")
341
+ logger.error(traceback.format_exc())
342
+ pil_image.save(out_filename, format='PNG')
343
+
344
+
345
+ def _convert_to_pil_image(image, color_order, verbose, logger):
346
+ if isinstance(image, np.ndarray):
347
+ if len(image.shape) == 3 and image.shape[2] == 3:
348
+ if color_order in ['auto', 'bgr']:
349
+ image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
350
+ if verbose:
351
+ logger.info(msg="Converted BGR to RGB for PIL")
352
+ return Image.fromarray(image_rgb)
353
+ return Image.fromarray(image)
354
+ return image
355
+
356
+
357
+ def _prepare_png_metadata(exif, verbose, logger):
358
+ pnginfo = PngInfo()
359
+ icc_profile = None
360
+ xmp_data = _extract_xmp_data(exif, verbose, logger)
361
+ if xmp_data:
362
+ pnginfo.add_text("XML:com.adobe.xmp", xmp_data)
363
+ if verbose:
364
+ logger.info(msg="Added XMP data to PNG info")
365
+ _add_exif_tags_to_pnginfo(exif, pnginfo, verbose, logger)
366
+ icc_profile = _extract_icc_profile(exif, verbose, logger)
367
+ return pnginfo, icc_profile
368
+
369
+
370
+ def _extract_xmp_data(exif, verbose, logger):
371
+ for key, value in exif.items():
372
+ if isinstance(key, str) and ('xmp' in key.lower() or 'xml' in key.lower()):
373
+ if isinstance(value, bytes):
374
+ try:
375
+ xmp_data = value.decode('utf-8', errors='ignore')
376
+ if verbose:
377
+ logger.info(msg=f"Found existing XMP data in source: {key}")
378
+ return xmp_data
379
+ except Exception:
380
+ continue
381
+ elif isinstance(value, str):
382
+ if verbose:
383
+ logger.info(msg=f"Found existing XMP data in source: {key}")
384
+ return value
385
+ if verbose:
386
+ logger.info("Generated new XMP data from EXIF")
387
+ return create_xmp_from_exif(exif)
388
+
389
+
390
+ def _add_exif_tags_to_pnginfo(exif, pnginfo, verbose, logger):
391
+ for tag_id, value in exif.items():
392
+ if value is None:
393
+ continue
394
+ if isinstance(tag_id, int):
395
+ _add_exif_tag(pnginfo, tag_id, value, verbose, logger)
396
+ elif isinstance(tag_id, str) and not tag_id.lower().startswith(('xmp', 'xml')):
397
+ _add_png_text_tag(pnginfo, tag_id, value, verbose, logger)
398
+
399
+
400
+ def _add_exif_tag(pnginfo, tag_id, value, verbose, logger):
401
+ try:
402
+ tag_name = TAGS.get(tag_id, f"Unknown_{tag_id}")
403
+ if isinstance(value, bytes) and len(value) > 1000:
404
+ return
405
+ if isinstance(value, (int, float, str)):
406
+ pnginfo.add_text(tag_name, str(value))
407
+ elif isinstance(value, bytes):
408
+ try:
409
+ decoded_value = value.decode('utf-8', errors='replace')
410
+ pnginfo.add_text(tag_name, decoded_value)
411
+ except Exception:
412
+ pass
413
+ elif hasattr(value, 'numerator'): # IFDRational
414
+ rational_str = f"{value.numerator}/{value.denominator}"
415
+ pnginfo.add_text(tag_name, rational_str)
416
+ else:
417
+ pnginfo.add_text(tag_name, str(value))
418
+ except Exception as e:
419
+ if verbose:
420
+ logger.warning(f"Could not store EXIF tag {tag_id}: {e}")
421
+
422
+
423
+ def _add_png_text_tag(pnginfo, key, value, verbose, logger):
424
+ try:
425
+ clean_key = key[4:] if key.startswith('PNG_') else key
426
+ if 'icc' in clean_key.lower() or 'profile' in clean_key.lower():
427
+ return
428
+ if isinstance(value, bytes):
429
+ try:
430
+ decoded_value = value.decode('utf-8', errors='replace')
431
+ pnginfo.add_text(clean_key, decoded_value)
432
+ except Exception:
433
+ truncated_value = str(value)[:100] + "..."
434
+ pnginfo.add_text(clean_key, truncated_value)
435
+ else:
436
+ pnginfo.add_text(clean_key, str(value))
437
+ except Exception as e:
438
+ if verbose:
439
+ logger.warning(msg=f"Could not store PNG metadata {key}: {e}")
154
440
 
155
441
 
156
- def write_image_with_exif_data(exif, image, out_filename, verbose=False):
442
+ def _extract_icc_profile(exif, verbose, logger):
443
+ for key, value in exif.items():
444
+ if (isinstance(key, str) and
445
+ isinstance(value, bytes) and
446
+ ('icc' in key.lower() or 'profile' in key.lower())):
447
+ if verbose:
448
+ logger.info(f"Found ICC profile: {key}")
449
+ return value
450
+ return None
451
+
452
+
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)
464
+
465
+
466
+ def write_image_with_exif_data(exif, image, out_filename, verbose=False, color_order='auto'):
157
467
  if exif is None:
158
468
  write_img(out_filename, image)
159
469
  return None
160
470
  if verbose:
161
471
  print_exif(exif)
162
472
  if extension_jpg(out_filename):
163
- cv2.imwrite(out_filename, image, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
164
- add_exif_data_to_jpg_file(exif, out_filename, out_filename, verbose)
473
+ write_image_with_exif_data_jpg(exif, image, out_filename, verbose)
165
474
  elif extension_tif(out_filename):
166
- metadata = {"description": f"image generated with {constants.APP_STRING} package"}
167
- extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
168
- tifffile.imwrite(out_filename, image, metadata=metadata, compression='adobe_deflate',
169
- extratags=extra_tags, **exif_tags)
475
+ write_image_with_exif_data_tif(exif, image, out_filename)
170
476
  elif extension_png(out_filename):
171
- image.save(out_filename, 'PNG', exif=exif, quality=100)
477
+ write_image_with_exif_data_png(exif, image, out_filename, verbose, color_order=color_order)
172
478
  return exif
173
479
 
174
480
 
@@ -181,8 +487,10 @@ def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
181
487
  print_exif(exif)
182
488
  if extension_tif(in_filename):
183
489
  image_new = tifffile.imread(in_filename)
184
- else:
490
+ elif extension_jpg(in_filename):
185
491
  image_new = Image.open(in_filename)
492
+ elif extension_png(in_filename):
493
+ image_new = cv2.imread(in_filename, cv2.IMREAD_UNCHANGED)
186
494
  if extension_jpg(in_filename):
187
495
  add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose)
188
496
  elif extension_tif(in_filename):
@@ -191,7 +499,7 @@ def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
191
499
  tifffile.imwrite(out_filename, image_new, metadata=metadata, compression='adobe_deflate',
192
500
  extratags=extra_tags, **exif_tags)
193
501
  elif extension_png(in_filename):
194
- image_new.save(out_filename, 'PNG', exif=exif, quality=100)
502
+ write_image_with_exif_data_png(exif, image_new, out_filename, verbose)
195
503
  return exif
196
504
 
197
505
 
@@ -204,22 +512,13 @@ def copy_exif_from_file_to_file(exif_filename, in_filename, out_filename=None, v
204
512
  return save_exif_data(exif, in_filename, out_filename, verbose)
205
513
 
206
514
 
207
- def exif_dict(exif, hide_xml=True):
515
+ def exif_dict(exif):
208
516
  if exif is None:
209
517
  return None
210
518
  exif_data = {}
211
519
  for tag_id in exif:
212
520
  tag = TAGS.get(tag_id, tag_id)
213
- if tag_id == XMLPACKET and hide_xml:
214
- data = "<<< XML data >>>"
215
- elif tag_id in (IMAGERESOURCES, INTERCOLORPROFILE):
216
- data = "<<< Photoshop data >>>"
217
- elif tag_id == STRIPOFFSETS:
218
- data = "<<< Strip offsets >>>"
219
- elif tag_id == STRIPBYTECOUNTS:
220
- data = "<<< Strip byte counts >>>"
221
- else:
222
- 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]
223
522
  if isinstance(data, bytes):
224
523
  try:
225
524
  data = data.decode()
@@ -229,12 +528,19 @@ def exif_dict(exif, hide_xml=True):
229
528
  return exif_data
230
529
 
231
530
 
232
- def print_exif(exif, hide_xml=True):
233
- exif_data = exif_dict(exif, hide_xml)
531
+ def print_exif(exif):
532
+ exif_data = exif_dict(exif)
234
533
  if exif_data is None:
235
534
  raise RuntimeError('Image has no exif data.')
236
535
  logger = logging.getLogger(__name__)
237
536
  for tag, (tag_id, data) in exif_data.items():
238
537
  if isinstance(data, IFDRational):
239
538
  data = f"{data.numerator}/{data.denominator}"
240
- logger.info(msg=f"{tag:25} [#{tag_id:5d}]: {data}")
539
+ data_str = f"{data}"
540
+ if len(data_str) > 40:
541
+ data_str = f"{data_str[:40]}..."
542
+ if isinstance(tag_id, int):
543
+ tag_id_str = f"[#{tag_id:5d}]"
544
+ else:
545
+ tag_id_str = f"[ {tag_id:20} ]"
546
+ logger.info(msg=f"{tag:25} {tag_id_str}: {data_str}")
@@ -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
- # pylint: disable=C0114, C0115, C0116, R0913, R0917
1
+ # pylint: disable=C0114, C0115, C0116, R0913, R0917, W0718
2
2
  import os
3
3
  import traceback
4
+ 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: