shinestacker 1.9.1__py3-none-any.whl → 1.9.2__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/align.py +18 -1
- shinestacker/algorithms/align_parallel.py +9 -5
- shinestacker/algorithms/exif.py +434 -251
- shinestacker/algorithms/stack.py +7 -3
- shinestacker/gui/main_window.py +6 -0
- shinestacker/gui/menu_manager.py +8 -2
- shinestacker/retouch/brush_tool.py +20 -0
- shinestacker/retouch/image_editor_ui.py +34 -1
- shinestacker/retouch/image_viewer.py +6 -1
- shinestacker/retouch/io_gui_handler.py +0 -1
- shinestacker/retouch/shortcuts_help.py +15 -8
- shinestacker/retouch/view_strategy.py +12 -2
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.2.dist-info}/METADATA +1 -1
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.2.dist-info}/RECORD +19 -19
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.2.dist-info}/WHEEL +0 -0
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.2.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.2.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.2.dist-info}/top_level.txt +0 -0
shinestacker/algorithms/exif.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0116, W0718, R0911, R0912, E1101, R0915, R1702, R0914, R0917, R0913
|
|
2
2
|
import os
|
|
3
|
+
import re
|
|
3
4
|
import logging
|
|
4
|
-
import traceback
|
|
5
5
|
import cv2
|
|
6
6
|
import numpy as np
|
|
7
7
|
from PIL import Image
|
|
@@ -34,21 +34,37 @@ NO_COPY_TIFF_TAGS_ID = [IMAGEWIDTH, IMAGELENGTH, RESOLUTIONX, RESOLUTIONY, BITSP
|
|
|
34
34
|
NO_COPY_TIFF_TAGS = ["Compression", "StripOffsets", "RowsPerStrip", "StripByteCounts"]
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
XMP_TEMPLATE = """<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
|
38
|
+
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21'>
|
|
39
|
+
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
|
|
40
|
+
<rdf:Description rdf:about='' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:xmp='http://ns.adobe.com/xap/1.0/' xmlns:tiff='http://ns.adobe.com/tiff/1.0/' xmlns:exif='http://ns.adobe.com/exif/1.0/' xmlns:aux='http://ns.adobe.com/exif/1.0/aux/'>
|
|
41
|
+
{content}
|
|
42
|
+
</rdf:Description>
|
|
43
|
+
</rdf:RDF>
|
|
44
|
+
</x:xmpmeta>
|
|
45
|
+
<?xpacket end='w'?>""" # noqa
|
|
46
|
+
|
|
47
|
+
XMP_EMPTY_TEMPLATE = """<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
|
48
|
+
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21'>
|
|
49
|
+
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
|
|
50
|
+
<rdf:Description rdf:about=''/>
|
|
51
|
+
</rdf:RDF>
|
|
52
|
+
</x:xmpmeta>
|
|
53
|
+
<?xpacket end='w'?>""" # noqa
|
|
54
|
+
|
|
55
|
+
|
|
37
56
|
def extract_enclosed_data_for_jpg(data, head, foot):
|
|
38
|
-
|
|
39
|
-
|
|
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:
|
|
57
|
+
xmp_start = data.find(head)
|
|
58
|
+
if xmp_start == -1:
|
|
48
59
|
return None
|
|
60
|
+
xmp_end = data.find(foot, xmp_start)
|
|
61
|
+
if xmp_end == -1:
|
|
62
|
+
return None
|
|
63
|
+
xmp_end += len(foot)
|
|
64
|
+
return data[xmp_start:xmp_end]
|
|
49
65
|
|
|
50
66
|
|
|
51
|
-
def get_exif(exif_filename):
|
|
67
|
+
def get_exif(exif_filename, enhanced_png_parsing=True):
|
|
52
68
|
if not os.path.isfile(exif_filename):
|
|
53
69
|
raise RuntimeError(f"File does not exist: {exif_filename}")
|
|
54
70
|
image = Image.open(exif_filename)
|
|
@@ -56,12 +72,20 @@ def get_exif(exif_filename):
|
|
|
56
72
|
return image.tag_v2 if hasattr(image, 'tag_v2') else image.getexif()
|
|
57
73
|
if extension_jpg(exif_filename):
|
|
58
74
|
exif_data = image.getexif()
|
|
75
|
+
try:
|
|
76
|
+
exif_subifd = image.getexif().get_ifd(34665)
|
|
77
|
+
exif_data.update(exif_subifd)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass # EXIF SubIFD is optional
|
|
59
80
|
with open(exif_filename, 'rb') as f:
|
|
60
|
-
data = extract_enclosed_data_for_jpg(
|
|
81
|
+
data = extract_enclosed_data_for_jpg(
|
|
82
|
+
f.read(), b'<?xpacket', b'<?xpacket end="w"?>')
|
|
61
83
|
if data is not None:
|
|
62
84
|
exif_data[XMLPACKET] = data
|
|
63
85
|
return exif_data
|
|
64
86
|
if extension_png(exif_filename):
|
|
87
|
+
if enhanced_png_parsing:
|
|
88
|
+
return get_enhanced_exif_from_png(image)
|
|
65
89
|
exif_data = get_exif_from_png(image)
|
|
66
90
|
return exif_data if exif_data else image.getexif()
|
|
67
91
|
return image.getexif()
|
|
@@ -69,25 +93,137 @@ def get_exif(exif_filename):
|
|
|
69
93
|
|
|
70
94
|
def get_exif_from_png(image):
|
|
71
95
|
exif_data = {}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
for key, value in image.text.items():
|
|
96
|
+
exif_from_image = image.getexif()
|
|
97
|
+
if exif_from_image:
|
|
98
|
+
exif_data.update(dict(exif_from_image))
|
|
99
|
+
for attr_name in ['text', 'info']:
|
|
100
|
+
if hasattr(image, attr_name) and getattr(image, attr_name):
|
|
101
|
+
for key, value in getattr(image, attr_name).items():
|
|
102
|
+
if attr_name == 'info' and key in ['dpi', 'gamma']:
|
|
103
|
+
continue
|
|
81
104
|
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
105
|
return exif_data
|
|
89
106
|
|
|
90
107
|
|
|
108
|
+
def parse_xmp_to_exif(xmp_data):
|
|
109
|
+
exif_data = {}
|
|
110
|
+
if not xmp_data:
|
|
111
|
+
return exif_data
|
|
112
|
+
if isinstance(xmp_data, bytes):
|
|
113
|
+
xmp_data = xmp_data.decode('utf-8', errors='ignore')
|
|
114
|
+
xmp_to_exif_map = {
|
|
115
|
+
'tiff:Make': 271, 'tiff:Model': 272, 'exif:ExposureTime': 33434,
|
|
116
|
+
'exif:FNumber': 33437, 'exif:ISOSpeedRatings': 34855, 'exif:FocalLength': 37386,
|
|
117
|
+
'exif:DateTimeOriginal': 36867, 'xmp:CreateDate': 306, 'xmp:CreatorTool': 305,
|
|
118
|
+
'aux:Lens': 42036, 'exif:Flash': 37385, 'exif:WhiteBalance': 41987,
|
|
119
|
+
'dc:description': 270, 'dc:creator': 315, 'dc:rights': 33432,
|
|
120
|
+
'exif:ShutterSpeedValue': 37377, 'exif:ApertureValue': 37378,
|
|
121
|
+
'exif:ExposureBiasValue': 37380, 'exif:MaxApertureValue': 37381,
|
|
122
|
+
'exif:MeteringMode': 37383, 'exif:ExposureMode': 41986,
|
|
123
|
+
'exif:SceneCaptureType': 41990
|
|
124
|
+
}
|
|
125
|
+
for xmp_tag, exif_tag in xmp_to_exif_map.items():
|
|
126
|
+
start_tag = f'<{xmp_tag}>'
|
|
127
|
+
end_tag = f'</{xmp_tag}>'
|
|
128
|
+
if start_tag in xmp_data:
|
|
129
|
+
start = xmp_data.find(start_tag) + len(start_tag)
|
|
130
|
+
end = xmp_data.find(end_tag, start)
|
|
131
|
+
if end != -1:
|
|
132
|
+
value = xmp_data[start:end].strip()
|
|
133
|
+
if value:
|
|
134
|
+
exif_data[exif_tag] = _parse_xmp_value(exif_tag, value)
|
|
135
|
+
return exif_data
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_xmp_value(exif_tag, value):
|
|
139
|
+
if exif_tag in [33434, 33437, 37386]: # Rational values
|
|
140
|
+
if '/' in value:
|
|
141
|
+
num, den = value.split('/')
|
|
142
|
+
try:
|
|
143
|
+
return IFDRational(int(num), int(den))
|
|
144
|
+
except (ValueError, ZeroDivisionError):
|
|
145
|
+
return float(value) if value else 0.0
|
|
146
|
+
return float(value) if value else 0.0
|
|
147
|
+
if exif_tag == 34855: # ISO
|
|
148
|
+
if '<rdf:li>' in value:
|
|
149
|
+
matches = re.findall(r'<rdf:li>([^<]+)</rdf:li>', value)
|
|
150
|
+
if matches:
|
|
151
|
+
value = matches[0]
|
|
152
|
+
try:
|
|
153
|
+
return int(value)
|
|
154
|
+
except ValueError:
|
|
155
|
+
return value
|
|
156
|
+
if exif_tag in [306, 36867]: # DateTime and DateTimeOriginal
|
|
157
|
+
if 'T' in value:
|
|
158
|
+
value = value.replace('T', ' ').replace('-', ':')
|
|
159
|
+
return value
|
|
160
|
+
return value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_typed_png_text(value):
|
|
164
|
+
if isinstance(value, str):
|
|
165
|
+
if value.startswith('RATIONAL:'):
|
|
166
|
+
parts = value[9:].split('/')
|
|
167
|
+
if len(parts) == 2:
|
|
168
|
+
try:
|
|
169
|
+
return IFDRational(int(parts[0]), int(parts[1]))
|
|
170
|
+
except (ValueError, ZeroDivisionError):
|
|
171
|
+
return value
|
|
172
|
+
elif value.startswith('INT:'):
|
|
173
|
+
try:
|
|
174
|
+
return int(value[4:])
|
|
175
|
+
except ValueError:
|
|
176
|
+
return value[4:]
|
|
177
|
+
elif value.startswith('FLOAT:'):
|
|
178
|
+
try:
|
|
179
|
+
return float(value[6:])
|
|
180
|
+
except ValueError:
|
|
181
|
+
return value[6:]
|
|
182
|
+
elif value.startswith('STRING:'):
|
|
183
|
+
return value[7:]
|
|
184
|
+
elif value.startswith('BYTES:'):
|
|
185
|
+
return value[6:].encode('utf-8')
|
|
186
|
+
elif value.startswith('ARRAY:'):
|
|
187
|
+
return [x.strip() for x in value[6:].split(',')]
|
|
188
|
+
return value
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_enhanced_exif_from_png(image):
|
|
192
|
+
basic_exif = get_exif_from_png(image)
|
|
193
|
+
enhanced_exif = {}
|
|
194
|
+
enhanced_exif.update(basic_exif)
|
|
195
|
+
xmp_data = None
|
|
196
|
+
if hasattr(image, 'text') and image.text:
|
|
197
|
+
xmp_data = image.text.get('XML:com.adobe.xmp') or image.text.get('xml:com.adobe.xmp')
|
|
198
|
+
if not xmp_data and 700 in basic_exif:
|
|
199
|
+
xmp_data = basic_exif[700]
|
|
200
|
+
if xmp_data:
|
|
201
|
+
enhanced_exif.update(parse_xmp_to_exif(xmp_data))
|
|
202
|
+
if hasattr(image, 'text') and image.text:
|
|
203
|
+
for key, value in image.text.items():
|
|
204
|
+
if key.startswith('EXIF_'):
|
|
205
|
+
parsed_value = parse_typed_png_text(value)
|
|
206
|
+
tag_id = _get_tag_id_from_png_key(key)
|
|
207
|
+
if tag_id:
|
|
208
|
+
enhanced_exif[tag_id] = parsed_value
|
|
209
|
+
return {k: v for k, v in enhanced_exif.items() if isinstance(k, int)}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _get_tag_id_from_png_key(key):
|
|
213
|
+
tag_map = {
|
|
214
|
+
'EXIF_CameraMake': 271, 'EXIF_CameraModel': 272, 'EXIF_Software': 305,
|
|
215
|
+
'EXIF_DateTime': 306, 'EXIF_Artist': 315, 'EXIF_Copyright': 33432,
|
|
216
|
+
'EXIF_ExposureTime': 33434, 'EXIF_FNumber': 33437, 'EXIF_ISOSpeed': 34855,
|
|
217
|
+
'EXIF_ShutterSpeedValue': 37377, 'EXIF_ApertureValue': 37378,
|
|
218
|
+
'EXIF_FocalLength': 37386, 'EXIF_LensModel': 42036,
|
|
219
|
+
'EXIF_ExposureBiasValue': 37380, 'EXIF_MaxApertureValue': 37381,
|
|
220
|
+
'EXIF_MeteringMode': 37383, 'EXIF_Flash': 37385, 'EXIF_WhiteBalance': 41987,
|
|
221
|
+
'EXIF_ExposureMode': 41986, 'EXIF_SceneCaptureType': 41990,
|
|
222
|
+
'EXIF_DateTimeOriginal': 36867
|
|
223
|
+
}
|
|
224
|
+
return tag_map.get(key)
|
|
225
|
+
|
|
226
|
+
|
|
91
227
|
def safe_decode_bytes(data, encoding='utf-8'):
|
|
92
228
|
if not isinstance(data, bytes):
|
|
93
229
|
return data
|
|
@@ -97,50 +233,7 @@ def safe_decode_bytes(data, encoding='utf-8'):
|
|
|
97
233
|
return data.decode(enc, errors='strict')
|
|
98
234
|
except UnicodeDecodeError:
|
|
99
235
|
continue
|
|
100
|
-
|
|
101
|
-
return data.decode('utf-8', errors='replace')
|
|
102
|
-
except Exception:
|
|
103
|
-
return "<<< decode error >>>"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def exif_extra_tags_for_tif(exif):
|
|
107
|
-
logger = logging.getLogger(__name__)
|
|
108
|
-
res_x, res_y = exif.get(RESOLUTIONX), exif.get(RESOLUTIONY)
|
|
109
|
-
if not (res_x is None or res_y is None):
|
|
110
|
-
resolution = ((res_x.numerator, res_x.denominator), (res_y.numerator, res_y.denominator))
|
|
111
|
-
else:
|
|
112
|
-
resolution = ((720000, 10000), (720000, 10000))
|
|
113
|
-
res_u = exif.get(RESOLUTIONUNIT)
|
|
114
|
-
resolutionunit = res_u if res_u is not None else 'inch'
|
|
115
|
-
sw = exif.get(SOFTWARE)
|
|
116
|
-
software = sw if sw is not None else "N/A"
|
|
117
|
-
phint = exif.get(PHOTOMETRICINTERPRETATION)
|
|
118
|
-
photometric = phint if phint is not None else None
|
|
119
|
-
extra = []
|
|
120
|
-
for tag_id in exif:
|
|
121
|
-
tag, data = TAGS.get(tag_id, tag_id), exif.get(tag_id)
|
|
122
|
-
if isinstance(data, bytes):
|
|
123
|
-
try:
|
|
124
|
-
if tag_id not in (IMAGERESOURCES, INTERCOLORPROFILE):
|
|
125
|
-
if tag_id == XMLPACKET:
|
|
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)
|
|
133
|
-
except Exception:
|
|
134
|
-
logger.warning(msg=f"Copy: can't decode EXIF tag {tag:25} [#{tag_id}]")
|
|
135
|
-
data = '<<< decode error >>>'
|
|
136
|
-
if isinstance(data, IFDRational):
|
|
137
|
-
data = (data.numerator, data.denominator)
|
|
138
|
-
if tag not in NO_COPY_TIFF_TAGS and tag_id not in NO_COPY_TIFF_TAGS_ID:
|
|
139
|
-
extra.append((tag_id, *get_tiff_dtype_count(data), data, False))
|
|
140
|
-
else:
|
|
141
|
-
logger.debug(msg=f"Skip tag {tag:25} [#{tag_id}]")
|
|
142
|
-
return extra, {'resolution': resolution, 'resolutionunit': resolutionunit,
|
|
143
|
-
'software': software, 'photometric': photometric}
|
|
236
|
+
return data.decode('utf-8', errors='replace')
|
|
144
237
|
|
|
145
238
|
|
|
146
239
|
def get_tiff_dtype_count(value):
|
|
@@ -173,32 +266,36 @@ def get_tiff_dtype_count(value):
|
|
|
173
266
|
|
|
174
267
|
|
|
175
268
|
def add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose=False):
|
|
176
|
-
logger = logging.getLogger(__name__)
|
|
177
269
|
if exif is None:
|
|
178
270
|
raise RuntimeError('No exif data provided.')
|
|
179
|
-
|
|
180
|
-
|
|
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]
|
|
271
|
+
logger = logging.getLogger(__name__)
|
|
272
|
+
xmp_data = exif.get(XMLPACKET) if hasattr(exif, 'get') else None
|
|
190
273
|
with Image.open(in_filename) as image:
|
|
191
|
-
if hasattr(exif, 'tobytes'):
|
|
274
|
+
if hasattr(exif, 'tobytes') and 'TiffImagePlugin' in str(type(exif)):
|
|
275
|
+
jpeg_exif = Image.Exif()
|
|
276
|
+
for tag_id in exif:
|
|
277
|
+
if tag_id != XMLPACKET:
|
|
278
|
+
try:
|
|
279
|
+
jpeg_exif[tag_id] = exif[tag_id]
|
|
280
|
+
except Exception as e:
|
|
281
|
+
if verbose:
|
|
282
|
+
logger.warning(msg=f"Failed to add tag {tag_id}: {e}")
|
|
283
|
+
exif_bytes = jpeg_exif.tobytes()
|
|
284
|
+
elif hasattr(exif, 'tobytes'):
|
|
192
285
|
exif_bytes = exif.tobytes()
|
|
193
286
|
else:
|
|
194
|
-
|
|
287
|
+
jpeg_exif = Image.Exif()
|
|
288
|
+
for tag_id, value in exif.items():
|
|
289
|
+
if tag_id != XMLPACKET:
|
|
290
|
+
try:
|
|
291
|
+
jpeg_exif[tag_id] = value
|
|
292
|
+
except Exception as e:
|
|
293
|
+
if verbose:
|
|
294
|
+
logger.warning(msg=f"Failed to add tag {tag_id}: {e}")
|
|
295
|
+
exif_bytes = jpeg_exif.tobytes()
|
|
195
296
|
image.save(out_filename, "JPEG", exif=exif_bytes, quality=100)
|
|
196
297
|
if xmp_data and isinstance(xmp_data, bytes):
|
|
197
|
-
|
|
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}")
|
|
298
|
+
_insert_xmp_into_jpeg(out_filename, xmp_data, verbose)
|
|
202
299
|
|
|
203
300
|
|
|
204
301
|
def _insert_xmp_into_jpeg(jpeg_path, xmp_data, verbose=False):
|
|
@@ -242,76 +339,85 @@ def _insert_xmp_into_jpeg(jpeg_path, xmp_data, verbose=False):
|
|
|
242
339
|
def create_xmp_from_exif(exif_data):
|
|
243
340
|
xmp_elements = []
|
|
244
341
|
if exif_data:
|
|
342
|
+
xmp_tag_map = {
|
|
343
|
+
270: {'format': 'dc:description', 'type': 'rdf_alt', 'processor': safe_decode_bytes},
|
|
344
|
+
315: {'format': 'dc:creator', 'type': 'rdf_seq', 'processor': safe_decode_bytes},
|
|
345
|
+
33432: {'format': 'dc:rights', 'type': 'rdf_alt', 'processor': safe_decode_bytes},
|
|
346
|
+
271: {'format': 'tiff:Make', 'type': 'simple', 'processor': safe_decode_bytes},
|
|
347
|
+
272: {'format': 'tiff:Model', 'type': 'simple', 'processor': safe_decode_bytes},
|
|
348
|
+
306: {'format': 'xmp:CreateDate', 'type': 'datetime', 'processor': safe_decode_bytes},
|
|
349
|
+
36867: {
|
|
350
|
+
'format': 'exif:DateTimeOriginal',
|
|
351
|
+
'type': 'datetime',
|
|
352
|
+
'processor': safe_decode_bytes
|
|
353
|
+
},
|
|
354
|
+
305: {'format': 'xmp:CreatorTool', 'type': 'simple', 'processor': safe_decode_bytes},
|
|
355
|
+
33434: {'format': 'exif:ExposureTime', 'type': 'rational', 'processor': None},
|
|
356
|
+
33437: {'format': 'exif:FNumber', 'type': 'rational', 'processor': None},
|
|
357
|
+
34855: {'format': 'exif:ISOSpeedRatings', 'type': 'rdf_seq', 'processor': None},
|
|
358
|
+
37386: {'format': 'exif:FocalLength', 'type': 'rational', 'processor': None},
|
|
359
|
+
42036: {'format': 'aux:Lens', 'type': 'simple', 'processor': safe_decode_bytes},
|
|
360
|
+
37377: {'format': 'exif:ShutterSpeedValue', 'type': 'rational', 'processor': None},
|
|
361
|
+
37378: {'format': 'exif:ApertureValue', 'type': 'rational', 'processor': None},
|
|
362
|
+
37380: {'format': 'exif:ExposureBiasValue', 'type': 'rational', 'processor': None},
|
|
363
|
+
37381: {'format': 'exif:MaxApertureValue', 'type': 'rational', 'processor': None},
|
|
364
|
+
37383: {'format': 'exif:MeteringMode', 'type': 'simple', 'processor': None},
|
|
365
|
+
37385: {'format': 'exif:Flash', 'type': 'simple', 'processor': None},
|
|
366
|
+
41987: {
|
|
367
|
+
'format': 'exif:WhiteBalance',
|
|
368
|
+
'type': 'mapped',
|
|
369
|
+
'processor': None,
|
|
370
|
+
'map': {0: 'Auto', 1: 'Manual'}
|
|
371
|
+
},
|
|
372
|
+
41986: {
|
|
373
|
+
'format': 'exif:ExposureMode',
|
|
374
|
+
'type': 'mapped',
|
|
375
|
+
'processor': None,
|
|
376
|
+
'map': {0: 'Auto', 1: 'Manual', 2: 'Auto bracket'}
|
|
377
|
+
},
|
|
378
|
+
41990: {
|
|
379
|
+
'format': 'exif:SceneCaptureType',
|
|
380
|
+
'type': 'mapped',
|
|
381
|
+
'processor': None,
|
|
382
|
+
'map': {0: 'Standard', 1: 'Landscape', 2: 'Portrait', 3: 'Night scene'}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
245
385
|
for tag_id, value in exif_data.items():
|
|
246
|
-
if isinstance(tag_id, int):
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
386
|
+
if isinstance(tag_id, int) and value and tag_id in xmp_tag_map:
|
|
387
|
+
config = xmp_tag_map[tag_id]
|
|
388
|
+
processed_value = config['processor'](value) if config['processor'] else value
|
|
389
|
+
if config['type'] == 'simple':
|
|
390
|
+
xmp_elements.append(
|
|
391
|
+
f'<{config["format"]}>{processed_value}</{config["format"]}>')
|
|
392
|
+
elif config['type'] == 'rdf_alt':
|
|
251
393
|
xmp_elements.append(
|
|
252
|
-
f'<
|
|
253
|
-
'
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if isinstance(artist, bytes):
|
|
257
|
-
artist = artist.decode('utf-8', errors='ignore')
|
|
394
|
+
f'<{config["format"]}><rdf:Alt>'
|
|
395
|
+
f'<rdf:li xml:lang="x-default">{processed_value}</rdf:li>'
|
|
396
|
+
f'</rdf:Alt></{config["format"]}>')
|
|
397
|
+
elif config['type'] == 'rdf_seq':
|
|
258
398
|
xmp_elements.append(
|
|
259
|
-
f'<
|
|
260
|
-
'</rdf:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if
|
|
264
|
-
|
|
399
|
+
f'<{config["format"]}><rdf:Seq>'
|
|
400
|
+
f'<rdf:li>{processed_value}</rdf:li>'
|
|
401
|
+
f'</rdf:Seq></{config["format"]}>')
|
|
402
|
+
elif config['type'] == 'datetime':
|
|
403
|
+
if ':' in processed_value:
|
|
404
|
+
processed_value = processed_value.replace(':', '-', 2).replace(' ', 'T')
|
|
265
405
|
xmp_elements.append(
|
|
266
|
-
f'<
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
elif
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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>')
|
|
406
|
+
f'<{config["format"]}>{processed_value}</{config["format"]}>')
|
|
407
|
+
elif config['type'] == 'rational':
|
|
408
|
+
float_value = float(value) \
|
|
409
|
+
if hasattr(value, 'numerator') \
|
|
410
|
+
else (float(value) if value else 0)
|
|
411
|
+
xmp_elements.append(
|
|
412
|
+
f'<{config["format"]}>{float_value}</{config["format"]}>')
|
|
413
|
+
elif config['type'] == 'mapped':
|
|
414
|
+
mapped_value = config['map'].get(value, str(value))
|
|
415
|
+
xmp_elements.append(
|
|
416
|
+
f'<{config["format"]}>{mapped_value}</{config["format"]}>')
|
|
290
417
|
if xmp_elements:
|
|
291
418
|
xmp_content = '\n '.join(xmp_elements)
|
|
292
|
-
|
|
293
|
-
|
|
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'?>"""
|
|
419
|
+
return XMP_TEMPLATE.format(content=xmp_content)
|
|
420
|
+
return XMP_EMPTY_TEMPLATE
|
|
315
421
|
|
|
316
422
|
|
|
317
423
|
def write_image_with_exif_data_png(exif, image, out_filename, verbose=False, color_order='auto'):
|
|
@@ -321,83 +427,92 @@ def write_image_with_exif_data_png(exif, image, out_filename, verbose=False, col
|
|
|
321
427
|
logger.warning(msg="EXIF data not supported for 16-bit PNG format")
|
|
322
428
|
write_img(out_filename, image)
|
|
323
429
|
return
|
|
324
|
-
pil_image = _convert_to_pil_image(image, color_order
|
|
325
|
-
pnginfo, icc_profile = _prepare_png_metadata(exif
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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')
|
|
430
|
+
pil_image = _convert_to_pil_image(image, color_order)
|
|
431
|
+
pnginfo, icc_profile = _prepare_png_metadata(exif)
|
|
432
|
+
save_args = {'format': 'PNG', 'pnginfo': pnginfo}
|
|
433
|
+
if icc_profile:
|
|
434
|
+
save_args['icc_profile'] = icc_profile
|
|
435
|
+
pil_image.save(out_filename, **save_args)
|
|
343
436
|
|
|
344
437
|
|
|
345
|
-
def _convert_to_pil_image(image, color_order
|
|
346
|
-
if isinstance(image, np.ndarray):
|
|
347
|
-
if
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
logger.info(msg="Converted BGR to RGB for PIL")
|
|
352
|
-
return Image.fromarray(image_rgb)
|
|
353
|
-
return Image.fromarray(image)
|
|
354
|
-
return image
|
|
438
|
+
def _convert_to_pil_image(image, color_order):
|
|
439
|
+
if isinstance(image, np.ndarray) and len(image.shape) == 3 and image.shape[2] == 3:
|
|
440
|
+
if color_order in ['auto', 'bgr']:
|
|
441
|
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
442
|
+
return Image.fromarray(image_rgb)
|
|
443
|
+
return Image.fromarray(image) if isinstance(image, np.ndarray) else image
|
|
355
444
|
|
|
356
445
|
|
|
357
|
-
def _prepare_png_metadata(exif
|
|
446
|
+
def _prepare_png_metadata(exif):
|
|
358
447
|
pnginfo = PngInfo()
|
|
359
448
|
icc_profile = None
|
|
360
|
-
xmp_data = _extract_xmp_data(exif
|
|
449
|
+
xmp_data = _extract_xmp_data(exif)
|
|
361
450
|
if xmp_data:
|
|
362
451
|
pnginfo.add_text("XML:com.adobe.xmp", xmp_data)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
_add_exif_tags_to_pnginfo(exif, pnginfo, verbose, logger)
|
|
366
|
-
icc_profile = _extract_icc_profile(exif, verbose, logger)
|
|
452
|
+
_add_exif_tags_to_pnginfo(exif, pnginfo)
|
|
453
|
+
icc_profile = _extract_icc_profile(exif)
|
|
367
454
|
return pnginfo, icc_profile
|
|
368
455
|
|
|
369
456
|
|
|
370
|
-
def _extract_xmp_data(exif
|
|
457
|
+
def _extract_xmp_data(exif):
|
|
371
458
|
for key, value in exif.items():
|
|
372
459
|
if isinstance(key, str) and ('xmp' in key.lower() or 'xml' in key.lower()):
|
|
373
460
|
if isinstance(value, bytes):
|
|
374
|
-
|
|
375
|
-
|
|
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}")
|
|
461
|
+
return value.decode('utf-8', errors='ignore')
|
|
462
|
+
if isinstance(value, str):
|
|
384
463
|
return value
|
|
385
|
-
if verbose:
|
|
386
|
-
logger.info("Generated new XMP data from EXIF")
|
|
387
464
|
return create_xmp_from_exif(exif)
|
|
388
465
|
|
|
389
466
|
|
|
390
|
-
def _add_exif_tags_to_pnginfo(exif, pnginfo
|
|
467
|
+
def _add_exif_tags_to_pnginfo(exif, pnginfo):
|
|
468
|
+
camera_tags = {
|
|
469
|
+
271: 'CameraMake', 272: 'CameraModel', 305: 'Software',
|
|
470
|
+
306: 'DateTime', 315: 'Artist', 33432: 'Copyright'
|
|
471
|
+
}
|
|
472
|
+
exposure_tags = {
|
|
473
|
+
33434: 'ExposureTime', 33437: 'FNumber', 34855: 'ISOSpeed',
|
|
474
|
+
37377: 'ShutterSpeedValue', 37378: 'ApertureValue', 37386: 'FocalLength',
|
|
475
|
+
42036: 'LensModel', 37380: 'ExposureBiasValue', 37381: 'MaxApertureValue',
|
|
476
|
+
37383: 'MeteringMode', 37385: 'Flash', 41987: 'WhiteBalance',
|
|
477
|
+
41986: 'ExposureMode', 41990: 'SceneCaptureType', 36867: 'DateTimeOriginal'
|
|
478
|
+
}
|
|
391
479
|
for tag_id, value in exif.items():
|
|
392
480
|
if value is None:
|
|
393
481
|
continue
|
|
394
482
|
if isinstance(tag_id, int):
|
|
395
|
-
|
|
483
|
+
if tag_id in camera_tags:
|
|
484
|
+
_add_typed_tag(pnginfo, f"EXIF_{camera_tags[tag_id]}", value)
|
|
485
|
+
elif tag_id in exposure_tags:
|
|
486
|
+
_add_typed_tag(pnginfo, f"EXIF_{exposure_tags[tag_id]}", value)
|
|
487
|
+
else:
|
|
488
|
+
_add_exif_tag(pnginfo, tag_id, value)
|
|
396
489
|
elif isinstance(tag_id, str) and not tag_id.lower().startswith(('xmp', 'xml')):
|
|
397
|
-
_add_png_text_tag(pnginfo, tag_id, value
|
|
490
|
+
_add_png_text_tag(pnginfo, tag_id, value)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _add_typed_tag(pnginfo, key, value):
|
|
494
|
+
try:
|
|
495
|
+
if hasattr(value, 'numerator'):
|
|
496
|
+
stored_value = f"RATIONAL:{value.numerator}/{value.denominator}"
|
|
497
|
+
elif isinstance(value, bytes):
|
|
498
|
+
try:
|
|
499
|
+
stored_value = f"STRING:{value.decode('utf-8', errors='replace')}"
|
|
500
|
+
except Exception:
|
|
501
|
+
stored_value = f"BYTES:{str(value)[:100]}"
|
|
502
|
+
elif isinstance(value, (list, tuple)):
|
|
503
|
+
stored_value = f"ARRAY:{','.join(str(x) for x in value)}"
|
|
504
|
+
elif isinstance(value, int):
|
|
505
|
+
stored_value = f"INT:{value}"
|
|
506
|
+
elif isinstance(value, float):
|
|
507
|
+
stored_value = f"FLOAT:{value}"
|
|
508
|
+
else:
|
|
509
|
+
stored_value = f"STRING:{str(value)}"
|
|
510
|
+
pnginfo.add_text(key, stored_value)
|
|
511
|
+
except Exception:
|
|
512
|
+
pass
|
|
398
513
|
|
|
399
514
|
|
|
400
|
-
def _add_exif_tag(pnginfo, tag_id, value
|
|
515
|
+
def _add_exif_tag(pnginfo, tag_id, value):
|
|
401
516
|
try:
|
|
402
517
|
tag_name = TAGS.get(tag_id, f"Unknown_{tag_id}")
|
|
403
518
|
if isinstance(value, bytes) and len(value) > 1000:
|
|
@@ -410,17 +525,16 @@ def _add_exif_tag(pnginfo, tag_id, value, verbose, logger):
|
|
|
410
525
|
pnginfo.add_text(tag_name, decoded_value)
|
|
411
526
|
except Exception:
|
|
412
527
|
pass
|
|
413
|
-
elif hasattr(value, 'numerator'):
|
|
528
|
+
elif hasattr(value, 'numerator'):
|
|
414
529
|
rational_str = f"{value.numerator}/{value.denominator}"
|
|
415
530
|
pnginfo.add_text(tag_name, rational_str)
|
|
416
531
|
else:
|
|
417
532
|
pnginfo.add_text(tag_name, str(value))
|
|
418
|
-
except Exception
|
|
419
|
-
|
|
420
|
-
logger.warning(f"Could not store EXIF tag {tag_id}: {e}")
|
|
533
|
+
except Exception:
|
|
534
|
+
pass
|
|
421
535
|
|
|
422
536
|
|
|
423
|
-
def _add_png_text_tag(pnginfo, key, value
|
|
537
|
+
def _add_png_text_tag(pnginfo, key, value):
|
|
424
538
|
try:
|
|
425
539
|
clean_key = key[4:] if key.startswith('PNG_') else key
|
|
426
540
|
if 'icc' in clean_key.lower() or 'profile' in clean_key.lower():
|
|
@@ -434,33 +548,97 @@ def _add_png_text_tag(pnginfo, key, value, verbose, logger):
|
|
|
434
548
|
pnginfo.add_text(clean_key, truncated_value)
|
|
435
549
|
else:
|
|
436
550
|
pnginfo.add_text(clean_key, str(value))
|
|
437
|
-
except Exception
|
|
438
|
-
|
|
439
|
-
logger.warning(msg=f"Could not store PNG metadata {key}: {e}")
|
|
551
|
+
except Exception:
|
|
552
|
+
pass
|
|
440
553
|
|
|
441
554
|
|
|
442
|
-
def _extract_icc_profile(exif
|
|
555
|
+
def _extract_icc_profile(exif):
|
|
443
556
|
for key, value in exif.items():
|
|
444
557
|
if (isinstance(key, str) and
|
|
445
558
|
isinstance(value, bytes) and
|
|
446
559
|
('icc' in key.lower() or 'profile' in key.lower())):
|
|
447
|
-
if verbose:
|
|
448
|
-
logger.info(f"Found ICC profile: {key}")
|
|
449
560
|
return value
|
|
450
561
|
return None
|
|
451
562
|
|
|
452
563
|
|
|
564
|
+
def clean_data_for_tiff(data):
|
|
565
|
+
if isinstance(data, str):
|
|
566
|
+
return data.encode('ascii', 'ignore').decode('ascii')
|
|
567
|
+
if isinstance(data, bytes):
|
|
568
|
+
decoded = data.decode('utf-8', 'ignore')
|
|
569
|
+
return decoded.encode('ascii', 'ignore').decode('ascii')
|
|
570
|
+
if isinstance(data, IFDRational):
|
|
571
|
+
return (data.numerator, data.denominator)
|
|
572
|
+
return data
|
|
573
|
+
|
|
574
|
+
|
|
453
575
|
def write_image_with_exif_data_jpg(exif, image, out_filename, verbose):
|
|
454
|
-
cv2.imwrite(out_filename, image, [
|
|
576
|
+
cv2.imwrite(out_filename, image, [cv2.IMWRITE_JPEG_QUALITY, 100])
|
|
455
577
|
add_exif_data_to_jpg_file(exif, out_filename, out_filename, verbose)
|
|
456
578
|
|
|
457
579
|
|
|
580
|
+
def exif_extra_tags_for_tif(exif):
|
|
581
|
+
res_x, res_y = exif.get(RESOLUTIONX), exif.get(RESOLUTIONY)
|
|
582
|
+
resolution = (
|
|
583
|
+
(res_x.numerator, res_x.denominator),
|
|
584
|
+
(res_y.numerator, res_y.denominator)
|
|
585
|
+
) if res_x and res_y else (
|
|
586
|
+
(720000, 10000), (720000, 10000)
|
|
587
|
+
)
|
|
588
|
+
exif_tags = {
|
|
589
|
+
'resolution': resolution,
|
|
590
|
+
'resolutionunit': exif.get(RESOLUTIONUNIT, 'inch'),
|
|
591
|
+
'software': clean_data_for_tiff(exif.get(SOFTWARE)) or constants.APP_TITLE,
|
|
592
|
+
'photometric': exif.get(PHOTOMETRICINTERPRETATION)
|
|
593
|
+
}
|
|
594
|
+
extra = []
|
|
595
|
+
for tag_id in exif:
|
|
596
|
+
tag, data = TAGS.get(tag_id, tag_id), exif.get(tag_id)
|
|
597
|
+
if tag in NO_COPY_TIFF_TAGS or tag_id in NO_COPY_TIFF_TAGS_ID or tag_id == SOFTWARE:
|
|
598
|
+
continue
|
|
599
|
+
if isinstance(data, IFDRational):
|
|
600
|
+
data = (data.numerator, data.denominator) if data.denominator != 0 else (0, 1)
|
|
601
|
+
extra.append((tag_id, 5, 1, data, False))
|
|
602
|
+
continue
|
|
603
|
+
processed_data = _process_tiff_data(data)
|
|
604
|
+
if processed_data:
|
|
605
|
+
dtype, count, data_value = processed_data
|
|
606
|
+
extra.append((tag_id, dtype, count, data_value, False))
|
|
607
|
+
return extra, exif_tags
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _process_tiff_data(data):
|
|
611
|
+
if isinstance(data, IFDRational):
|
|
612
|
+
data = (data.numerator, data.denominator) if data.denominator != 0 else (0, 1)
|
|
613
|
+
return 5, 1, data
|
|
614
|
+
if hasattr(data, '__iter__') and not isinstance(data, (str, bytes)):
|
|
615
|
+
try:
|
|
616
|
+
clean_data = [float(x)
|
|
617
|
+
if not hasattr(x, 'denominator') or x.denominator != 0
|
|
618
|
+
else float('nan') for x in data]
|
|
619
|
+
return 12, len(clean_data), tuple(clean_data)
|
|
620
|
+
except Exception:
|
|
621
|
+
return None
|
|
622
|
+
if isinstance(data, (str, bytes)):
|
|
623
|
+
clean_data = clean_data_for_tiff(data)
|
|
624
|
+
if clean_data:
|
|
625
|
+
return 2, len(clean_data) + 1, clean_data
|
|
626
|
+
try:
|
|
627
|
+
dtype, count = get_tiff_dtype_count(data)
|
|
628
|
+
return dtype, count, data
|
|
629
|
+
except Exception:
|
|
630
|
+
return None
|
|
631
|
+
|
|
632
|
+
|
|
458
633
|
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
634
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
462
|
-
|
|
463
|
-
|
|
635
|
+
try:
|
|
636
|
+
metadata = {"description": f"image generated with {constants.APP_STRING} package"}
|
|
637
|
+
extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
|
|
638
|
+
tifffile.imwrite(out_filename, image, metadata=metadata, compression='adobe_deflate',
|
|
639
|
+
extratags=extra_tags, **exif_tags)
|
|
640
|
+
except Exception:
|
|
641
|
+
tifffile.imwrite(out_filename, image, compression='adobe_deflate')
|
|
464
642
|
|
|
465
643
|
|
|
466
644
|
def write_image_with_exif_data(exif, image, out_filename, verbose=False, color_order='auto'):
|
|
@@ -485,21 +663,21 @@ def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
|
|
|
485
663
|
raise RuntimeError('No exif data provided.')
|
|
486
664
|
if verbose:
|
|
487
665
|
print_exif(exif)
|
|
488
|
-
if extension_tif(in_filename):
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
666
|
+
if extension_png(in_filename) or extension_tif(in_filename):
|
|
667
|
+
if extension_tif(in_filename):
|
|
668
|
+
image_new = tifffile.imread(in_filename)
|
|
669
|
+
elif extension_png(in_filename):
|
|
670
|
+
image_new = cv2.imread(in_filename, cv2.IMREAD_UNCHANGED)
|
|
671
|
+
if extension_tif(in_filename):
|
|
672
|
+
metadata = {"description": f"image generated with {constants.APP_STRING} package"}
|
|
673
|
+
extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
|
|
674
|
+
tifffile.imwrite(
|
|
675
|
+
out_filename, image_new, metadata=metadata, compression='adobe_deflate',
|
|
676
|
+
extratags=extra_tags, **exif_tags)
|
|
677
|
+
elif extension_png(in_filename):
|
|
678
|
+
write_image_with_exif_data_png(exif, image_new, out_filename, verbose)
|
|
679
|
+
else:
|
|
495
680
|
add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose)
|
|
496
|
-
elif extension_tif(in_filename):
|
|
497
|
-
metadata = {"description": f"image generated with {constants.APP_STRING} package"}
|
|
498
|
-
extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
|
|
499
|
-
tifffile.imwrite(out_filename, image_new, metadata=metadata, compression='adobe_deflate',
|
|
500
|
-
extratags=extra_tags, **exif_tags)
|
|
501
|
-
elif extension_png(in_filename):
|
|
502
|
-
write_image_with_exif_data_png(exif, image_new, out_filename, verbose)
|
|
503
681
|
return exif
|
|
504
682
|
|
|
505
683
|
|
|
@@ -512,20 +690,25 @@ def copy_exif_from_file_to_file(exif_filename, in_filename, out_filename=None, v
|
|
|
512
690
|
return save_exif_data(exif, in_filename, out_filename, verbose)
|
|
513
691
|
|
|
514
692
|
|
|
515
|
-
def exif_dict(
|
|
516
|
-
if
|
|
693
|
+
def exif_dict(exif_data):
|
|
694
|
+
if exif_data is None:
|
|
517
695
|
return None
|
|
518
|
-
|
|
519
|
-
for
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
696
|
+
result = {}
|
|
697
|
+
for tag, value in exif_data.items():
|
|
698
|
+
if isinstance(tag, int):
|
|
699
|
+
tag_name = TAGS.get(tag, str(tag))
|
|
700
|
+
else:
|
|
701
|
+
tag_name = str(tag)
|
|
702
|
+
if tag_name.startswith('PNG_EXIF_'):
|
|
703
|
+
standard_tag = tag_name[9:]
|
|
704
|
+
elif tag_name.startswith('EXIF_'):
|
|
705
|
+
standard_tag = tag_name[5:]
|
|
706
|
+
elif tag_name.startswith('PNG_'):
|
|
707
|
+
continue
|
|
708
|
+
else:
|
|
709
|
+
standard_tag = tag_name
|
|
710
|
+
result[standard_tag] = (tag, value)
|
|
711
|
+
return result
|
|
529
712
|
|
|
530
713
|
|
|
531
714
|
def print_exif(exif):
|
|
@@ -538,7 +721,7 @@ def print_exif(exif):
|
|
|
538
721
|
data = f"{data.numerator}/{data.denominator}"
|
|
539
722
|
data_str = f"{data}"
|
|
540
723
|
if len(data_str) > 40:
|
|
541
|
-
data_str = f"{data_str[:40]}..."
|
|
724
|
+
data_str = f"{data_str[:40]}... (truncated)"
|
|
542
725
|
if isinstance(tag_id, int):
|
|
543
726
|
tag_id_str = f"[#{tag_id:5d}]"
|
|
544
727
|
else:
|