shinestacker 1.9.0__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 +498 -255
- shinestacker/algorithms/stack.py +8 -4
- shinestacker/app/main.py +1 -1
- shinestacker/gui/action_config_dialog.py +0 -4
- shinestacker/gui/config_dialog.py +6 -5
- shinestacker/gui/main_window.py +6 -0
- shinestacker/gui/menu_manager.py +8 -2
- shinestacker/retouch/brush_tool.py +20 -0
- shinestacker/retouch/exif_data.py +57 -37
- shinestacker/retouch/image_editor_ui.py +79 -3
- shinestacker/retouch/image_viewer.py +6 -1
- shinestacker/retouch/io_gui_handler.py +9 -12
- shinestacker/retouch/shortcuts_help.py +15 -8
- shinestacker/retouch/view_strategy.py +12 -2
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/METADATA +1 -1
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/RECORD +23 -23
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/WHEEL +0 -0
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.9.0.dist-info → shinestacker-1.9.2.dist-info}/top_level.txt +0 -0
shinestacker/algorithms/exif.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0116, W0718, R0911, R0912, E1101, R0915, R1702, R0914, R0917, R0913
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
|
-
import io
|
|
5
4
|
import logging
|
|
6
|
-
import traceback
|
|
7
5
|
import cv2
|
|
8
6
|
import numpy as np
|
|
9
7
|
from PIL import Image
|
|
@@ -36,18 +34,37 @@ NO_COPY_TIFF_TAGS_ID = [IMAGEWIDTH, IMAGELENGTH, RESOLUTIONX, RESOLUTIONY, BITSP
|
|
|
36
34
|
NO_COPY_TIFF_TAGS = ["Compression", "StripOffsets", "RowsPerStrip", "StripByteCounts"]
|
|
37
35
|
|
|
38
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
|
+
|
|
39
56
|
def extract_enclosed_data_for_jpg(data, head, foot):
|
|
40
|
-
|
|
41
|
-
xmp_start
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
57
|
+
xmp_start = data.find(head)
|
|
58
|
+
if xmp_start == -1:
|
|
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]
|
|
48
65
|
|
|
49
66
|
|
|
50
|
-
def get_exif(exif_filename):
|
|
67
|
+
def get_exif(exif_filename, enhanced_png_parsing=True):
|
|
51
68
|
if not os.path.isfile(exif_filename):
|
|
52
69
|
raise RuntimeError(f"File does not exist: {exif_filename}")
|
|
53
70
|
image = Image.open(exif_filename)
|
|
@@ -55,12 +72,20 @@ def get_exif(exif_filename):
|
|
|
55
72
|
return image.tag_v2 if hasattr(image, 'tag_v2') else image.getexif()
|
|
56
73
|
if extension_jpg(exif_filename):
|
|
57
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
|
|
58
80
|
with open(exif_filename, 'rb') as f:
|
|
59
|
-
data = extract_enclosed_data_for_jpg(
|
|
81
|
+
data = extract_enclosed_data_for_jpg(
|
|
82
|
+
f.read(), b'<?xpacket', b'<?xpacket end="w"?>')
|
|
60
83
|
if data is not None:
|
|
61
84
|
exif_data[XMLPACKET] = data
|
|
62
85
|
return exif_data
|
|
63
86
|
if extension_png(exif_filename):
|
|
87
|
+
if enhanced_png_parsing:
|
|
88
|
+
return get_enhanced_exif_from_png(image)
|
|
64
89
|
exif_data = get_exif_from_png(image)
|
|
65
90
|
return exif_data if exif_data else image.getexif()
|
|
66
91
|
return image.getexif()
|
|
@@ -68,58 +93,147 @@ def get_exif(exif_filename):
|
|
|
68
93
|
|
|
69
94
|
def get_exif_from_png(image):
|
|
70
95
|
exif_data = {}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
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
|
|
80
104
|
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
105
|
return exif_data
|
|
88
106
|
|
|
89
107
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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('/')
|
|
107
142
|
try:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
|
|
227
|
+
def safe_decode_bytes(data, encoding='utf-8'):
|
|
228
|
+
if not isinstance(data, bytes):
|
|
229
|
+
return data
|
|
230
|
+
encodings = [encoding, 'latin-1', 'cp1252', 'utf-16', 'ascii']
|
|
231
|
+
for enc in encodings:
|
|
232
|
+
try:
|
|
233
|
+
return data.decode(enc, errors='strict')
|
|
234
|
+
except UnicodeDecodeError:
|
|
235
|
+
continue
|
|
236
|
+
return data.decode('utf-8', errors='replace')
|
|
123
237
|
|
|
124
238
|
|
|
125
239
|
def get_tiff_dtype_count(value):
|
|
@@ -151,107 +265,159 @@ def get_tiff_dtype_count(value):
|
|
|
151
265
|
return 2, len(str(value)) + 1 # Default for othre cases (ASCII string)
|
|
152
266
|
|
|
153
267
|
|
|
154
|
-
def add_exif_data_to_jpg_file(exif,
|
|
155
|
-
logger = logging.getLogger(__name__)
|
|
268
|
+
def add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose=False):
|
|
156
269
|
if exif is None:
|
|
157
270
|
raise RuntimeError('No exif data provided.')
|
|
271
|
+
logger = logging.getLogger(__name__)
|
|
272
|
+
xmp_data = exif.get(XMLPACKET) if hasattr(exif, 'get') else None
|
|
273
|
+
with Image.open(in_filename) as image:
|
|
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'):
|
|
285
|
+
exif_bytes = exif.tobytes()
|
|
286
|
+
else:
|
|
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()
|
|
296
|
+
image.save(out_filename, "JPEG", exif=exif_bytes, quality=100)
|
|
297
|
+
if xmp_data and isinstance(xmp_data, bytes):
|
|
298
|
+
_insert_xmp_into_jpeg(out_filename, xmp_data, verbose)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _insert_xmp_into_jpeg(jpeg_path, xmp_data, verbose=False):
|
|
302
|
+
logger = logging.getLogger(__name__)
|
|
303
|
+
with open(jpeg_path, 'rb') as f:
|
|
304
|
+
jpeg_data = f.read()
|
|
305
|
+
soi_pos = jpeg_data.find(b'\xFF\xD8')
|
|
306
|
+
if soi_pos == -1:
|
|
307
|
+
if verbose:
|
|
308
|
+
logger.warning("No SOI marker found, cannot insert XMP")
|
|
309
|
+
return
|
|
310
|
+
insert_pos = soi_pos + 2
|
|
311
|
+
current_pos = insert_pos
|
|
312
|
+
while current_pos < len(jpeg_data) - 4:
|
|
313
|
+
if jpeg_data[current_pos] != 0xFF:
|
|
314
|
+
break
|
|
315
|
+
marker = jpeg_data[current_pos + 1]
|
|
316
|
+
if marker == 0xDA:
|
|
317
|
+
break
|
|
318
|
+
segment_length = int.from_bytes(jpeg_data[current_pos + 2:current_pos + 4], 'big')
|
|
319
|
+
if marker == 0xE1:
|
|
320
|
+
insert_pos = current_pos + 2 + segment_length
|
|
321
|
+
current_pos = insert_pos
|
|
322
|
+
continue
|
|
323
|
+
current_pos += 2 + segment_length
|
|
324
|
+
xmp_identifier = b'http://ns.adobe.com/xap/1.0/\x00'
|
|
325
|
+
xmp_payload = xmp_identifier + xmp_data
|
|
326
|
+
segment_length = len(xmp_payload) + 2
|
|
327
|
+
xmp_segment = b'\xFF\xE1' + segment_length.to_bytes(2, 'big') + xmp_payload
|
|
328
|
+
updated_data = (
|
|
329
|
+
jpeg_data[:insert_pos] +
|
|
330
|
+
xmp_segment +
|
|
331
|
+
jpeg_data[insert_pos:]
|
|
332
|
+
)
|
|
333
|
+
with open(jpeg_path, 'wb') as f:
|
|
334
|
+
f.write(updated_data)
|
|
158
335
|
if verbose:
|
|
159
|
-
|
|
160
|
-
xmp_data = extract_enclosed_data_for_jpg(exif[XMLPACKET], b'<x:xmpmeta', b'</x:xmpmeta>')
|
|
161
|
-
with Image.open(in_filenama) as image:
|
|
162
|
-
with io.BytesIO() as buffer:
|
|
163
|
-
image.save(buffer, format="JPEG", exif=exif.tobytes(), quality=100)
|
|
164
|
-
jpeg_data = buffer.getvalue()
|
|
165
|
-
if xmp_data is not None:
|
|
166
|
-
app1_marker_pos = jpeg_data.find(b'\xFF\xE1')
|
|
167
|
-
if app1_marker_pos == -1:
|
|
168
|
-
app1_marker_pos = len(jpeg_data) - 2
|
|
169
|
-
updated_data = (
|
|
170
|
-
jpeg_data[:app1_marker_pos] +
|
|
171
|
-
b'\xFF\xE1' + len(xmp_data).to_bytes(2, 'big') +
|
|
172
|
-
xmp_data + jpeg_data[app1_marker_pos:]
|
|
173
|
-
)
|
|
174
|
-
else:
|
|
175
|
-
logger.warning("Copy: can't find XMLPacket in JPG EXIF data")
|
|
176
|
-
updated_data = jpeg_data
|
|
177
|
-
with open(out_filename, 'wb') as f:
|
|
178
|
-
f.write(updated_data)
|
|
179
|
-
return exif
|
|
336
|
+
logger.info("Successfully inserted XMP data into JPEG")
|
|
180
337
|
|
|
181
338
|
|
|
182
339
|
def create_xmp_from_exif(exif_data):
|
|
183
340
|
xmp_elements = []
|
|
184
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
|
+
}
|
|
185
385
|
for tag_id, value in exif_data.items():
|
|
186
|
-
if isinstance(tag_id, int):
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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':
|
|
393
|
+
xmp_elements.append(
|
|
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':
|
|
191
398
|
xmp_elements.append(
|
|
192
|
-
f'<
|
|
193
|
-
'</rdf:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if
|
|
197
|
-
|
|
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')
|
|
198
405
|
xmp_elements.append(
|
|
199
|
-
f'<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
copyright_tag = copyright_tag.decode('utf-8', errors='ignore')
|
|
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)
|
|
205
411
|
xmp_elements.append(
|
|
206
|
-
f'<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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>')
|
|
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"]}>')
|
|
230
417
|
if xmp_elements:
|
|
231
418
|
xmp_content = '\n '.join(xmp_elements)
|
|
232
|
-
|
|
233
|
-
|
|
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'?>"""
|
|
419
|
+
return XMP_TEMPLATE.format(content=xmp_content)
|
|
420
|
+
return XMP_EMPTY_TEMPLATE
|
|
255
421
|
|
|
256
422
|
|
|
257
423
|
def write_image_with_exif_data_png(exif, image, out_filename, verbose=False, color_order='auto'):
|
|
@@ -261,67 +427,92 @@ def write_image_with_exif_data_png(exif, image, out_filename, verbose=False, col
|
|
|
261
427
|
logger.warning(msg="EXIF data not supported for 16-bit PNG format")
|
|
262
428
|
write_img(out_filename, image)
|
|
263
429
|
return
|
|
264
|
-
pil_image = _convert_to_pil_image(image, color_order
|
|
265
|
-
pnginfo, icc_profile = _prepare_png_metadata(exif
|
|
266
|
-
|
|
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)
|
|
267
436
|
|
|
268
437
|
|
|
269
|
-
def _convert_to_pil_image(image, color_order
|
|
270
|
-
if isinstance(image, np.ndarray):
|
|
271
|
-
if
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
logger.info(msg="Converted BGR to RGB for PIL")
|
|
276
|
-
return Image.fromarray(image_rgb)
|
|
277
|
-
return Image.fromarray(image)
|
|
278
|
-
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
|
|
279
444
|
|
|
280
445
|
|
|
281
|
-
def _prepare_png_metadata(exif
|
|
446
|
+
def _prepare_png_metadata(exif):
|
|
282
447
|
pnginfo = PngInfo()
|
|
283
448
|
icc_profile = None
|
|
284
|
-
xmp_data = _extract_xmp_data(exif
|
|
449
|
+
xmp_data = _extract_xmp_data(exif)
|
|
285
450
|
if xmp_data:
|
|
286
451
|
pnginfo.add_text("XML:com.adobe.xmp", xmp_data)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
_add_exif_tags_to_pnginfo(exif, pnginfo, verbose, logger)
|
|
290
|
-
icc_profile = _extract_icc_profile(exif, verbose, logger)
|
|
452
|
+
_add_exif_tags_to_pnginfo(exif, pnginfo)
|
|
453
|
+
icc_profile = _extract_icc_profile(exif)
|
|
291
454
|
return pnginfo, icc_profile
|
|
292
455
|
|
|
293
456
|
|
|
294
|
-
def _extract_xmp_data(exif
|
|
457
|
+
def _extract_xmp_data(exif):
|
|
295
458
|
for key, value in exif.items():
|
|
296
459
|
if isinstance(key, str) and ('xmp' in key.lower() or 'xml' in key.lower()):
|
|
297
460
|
if isinstance(value, bytes):
|
|
298
|
-
|
|
299
|
-
|
|
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}")
|
|
461
|
+
return value.decode('utf-8', errors='ignore')
|
|
462
|
+
if isinstance(value, str):
|
|
308
463
|
return value
|
|
309
|
-
if verbose:
|
|
310
|
-
logger.info("Generated new XMP data from EXIF")
|
|
311
464
|
return create_xmp_from_exif(exif)
|
|
312
465
|
|
|
313
466
|
|
|
314
|
-
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
|
+
}
|
|
315
479
|
for tag_id, value in exif.items():
|
|
316
480
|
if value is None:
|
|
317
481
|
continue
|
|
318
482
|
if isinstance(tag_id, int):
|
|
319
|
-
|
|
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)
|
|
320
489
|
elif isinstance(tag_id, str) and not tag_id.lower().startswith(('xmp', 'xml')):
|
|
321
|
-
_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
|
|
322
513
|
|
|
323
514
|
|
|
324
|
-
def _add_exif_tag(pnginfo, tag_id, value
|
|
515
|
+
def _add_exif_tag(pnginfo, tag_id, value):
|
|
325
516
|
try:
|
|
326
517
|
tag_name = TAGS.get(tag_id, f"Unknown_{tag_id}")
|
|
327
518
|
if isinstance(value, bytes) and len(value) > 1000:
|
|
@@ -334,17 +525,16 @@ def _add_exif_tag(pnginfo, tag_id, value, verbose, logger):
|
|
|
334
525
|
pnginfo.add_text(tag_name, decoded_value)
|
|
335
526
|
except Exception:
|
|
336
527
|
pass
|
|
337
|
-
elif hasattr(value, 'numerator'):
|
|
528
|
+
elif hasattr(value, 'numerator'):
|
|
338
529
|
rational_str = f"{value.numerator}/{value.denominator}"
|
|
339
530
|
pnginfo.add_text(tag_name, rational_str)
|
|
340
531
|
else:
|
|
341
532
|
pnginfo.add_text(tag_name, str(value))
|
|
342
|
-
except Exception
|
|
343
|
-
|
|
344
|
-
logger.warning(f"Could not store EXIF tag {tag_id}: {e}")
|
|
533
|
+
except Exception:
|
|
534
|
+
pass
|
|
345
535
|
|
|
346
536
|
|
|
347
|
-
def _add_png_text_tag(pnginfo, key, value
|
|
537
|
+
def _add_png_text_tag(pnginfo, key, value):
|
|
348
538
|
try:
|
|
349
539
|
clean_key = key[4:] if key.startswith('PNG_') else key
|
|
350
540
|
if 'icc' in clean_key.lower() or 'profile' in clean_key.lower():
|
|
@@ -358,40 +548,97 @@ def _add_png_text_tag(pnginfo, key, value, verbose, logger):
|
|
|
358
548
|
pnginfo.add_text(clean_key, truncated_value)
|
|
359
549
|
else:
|
|
360
550
|
pnginfo.add_text(clean_key, str(value))
|
|
361
|
-
except Exception
|
|
362
|
-
|
|
363
|
-
logger.warning(msg=f"Could not store PNG metadata {key}: {e}")
|
|
551
|
+
except Exception:
|
|
552
|
+
pass
|
|
364
553
|
|
|
365
554
|
|
|
366
|
-
def _extract_icc_profile(exif
|
|
555
|
+
def _extract_icc_profile(exif):
|
|
367
556
|
for key, value in exif.items():
|
|
368
557
|
if (isinstance(key, str) and
|
|
369
558
|
isinstance(value, bytes) and
|
|
370
559
|
('icc' in key.lower() or 'profile' in key.lower())):
|
|
371
|
-
if verbose:
|
|
372
|
-
logger.info(f"Found ICC profile: {key}")
|
|
373
560
|
return value
|
|
374
561
|
return None
|
|
375
562
|
|
|
376
563
|
|
|
377
|
-
def
|
|
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
|
+
|
|
575
|
+
def write_image_with_exif_data_jpg(exif, image, out_filename, verbose):
|
|
576
|
+
cv2.imwrite(out_filename, image, [cv2.IMWRITE_JPEG_QUALITY, 100])
|
|
577
|
+
add_exif_data_to_jpg_file(exif, out_filename, out_filename, verbose)
|
|
578
|
+
|
|
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
|
+
|
|
633
|
+
def write_image_with_exif_data_tif(exif, image, out_filename):
|
|
634
|
+
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
378
635
|
try:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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')
|
|
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')
|
|
395
642
|
|
|
396
643
|
|
|
397
644
|
def write_image_with_exif_data(exif, image, out_filename, verbose=False, color_order='auto'):
|
|
@@ -401,13 +648,9 @@ def write_image_with_exif_data(exif, image, out_filename, verbose=False, color_o
|
|
|
401
648
|
if verbose:
|
|
402
649
|
print_exif(exif)
|
|
403
650
|
if extension_jpg(out_filename):
|
|
404
|
-
|
|
405
|
-
add_exif_data_to_jpg_file(exif, out_filename, out_filename, verbose)
|
|
651
|
+
write_image_with_exif_data_jpg(exif, image, out_filename, verbose)
|
|
406
652
|
elif extension_tif(out_filename):
|
|
407
|
-
|
|
408
|
-
extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
|
|
409
|
-
tifffile.imwrite(out_filename, image, metadata=metadata, compression='adobe_deflate',
|
|
410
|
-
extratags=extra_tags, **exif_tags)
|
|
653
|
+
write_image_with_exif_data_tif(exif, image, out_filename)
|
|
411
654
|
elif extension_png(out_filename):
|
|
412
655
|
write_image_with_exif_data_png(exif, image, out_filename, verbose, color_order=color_order)
|
|
413
656
|
return exif
|
|
@@ -420,21 +663,21 @@ def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
|
|
|
420
663
|
raise RuntimeError('No exif data provided.')
|
|
421
664
|
if verbose:
|
|
422
665
|
print_exif(exif)
|
|
423
|
-
if extension_tif(in_filename):
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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:
|
|
430
680
|
add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose)
|
|
431
|
-
elif extension_tif(in_filename):
|
|
432
|
-
metadata = {"description": f"image generated with {constants.APP_STRING} package"}
|
|
433
|
-
extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
|
|
434
|
-
tifffile.imwrite(out_filename, image_new, metadata=metadata, compression='adobe_deflate',
|
|
435
|
-
extratags=extra_tags, **exif_tags)
|
|
436
|
-
elif extension_png(in_filename):
|
|
437
|
-
write_image_with_exif_data_png(exif, image_new, out_filename, verbose)
|
|
438
681
|
return exif
|
|
439
682
|
|
|
440
683
|
|
|
@@ -447,40 +690,40 @@ def copy_exif_from_file_to_file(exif_filename, in_filename, out_filename=None, v
|
|
|
447
690
|
return save_exif_data(exif, in_filename, out_filename, verbose)
|
|
448
691
|
|
|
449
692
|
|
|
450
|
-
def exif_dict(
|
|
451
|
-
if
|
|
693
|
+
def exif_dict(exif_data):
|
|
694
|
+
if exif_data is None:
|
|
452
695
|
return None
|
|
453
|
-
|
|
454
|
-
for
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
data = "<<< XML data >>>"
|
|
458
|
-
elif tag_id in (IMAGERESOURCES, INTERCOLORPROFILE):
|
|
459
|
-
data = "<<< Photoshop data >>>"
|
|
460
|
-
elif tag_id == STRIPOFFSETS:
|
|
461
|
-
data = "<<< Strip offsets >>>"
|
|
462
|
-
elif tag_id == STRIPBYTECOUNTS:
|
|
463
|
-
data = "<<< Strip byte counts >>>"
|
|
696
|
+
result = {}
|
|
697
|
+
for tag, value in exif_data.items():
|
|
698
|
+
if isinstance(tag, int):
|
|
699
|
+
tag_name = TAGS.get(tag, str(tag))
|
|
464
700
|
else:
|
|
465
|
-
|
|
466
|
-
if
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
|
473
712
|
|
|
474
713
|
|
|
475
|
-
def print_exif(exif
|
|
476
|
-
exif_data = exif_dict(exif
|
|
714
|
+
def print_exif(exif):
|
|
715
|
+
exif_data = exif_dict(exif)
|
|
477
716
|
if exif_data is None:
|
|
478
717
|
raise RuntimeError('Image has no exif data.')
|
|
479
718
|
logger = logging.getLogger(__name__)
|
|
480
719
|
for tag, (tag_id, data) in exif_data.items():
|
|
481
720
|
if isinstance(data, IFDRational):
|
|
482
721
|
data = f"{data.numerator}/{data.denominator}"
|
|
722
|
+
data_str = f"{data}"
|
|
723
|
+
if len(data_str) > 40:
|
|
724
|
+
data_str = f"{data_str[:40]}... (truncated)"
|
|
483
725
|
if isinstance(tag_id, int):
|
|
484
|
-
|
|
726
|
+
tag_id_str = f"[#{tag_id:5d}]"
|
|
485
727
|
else:
|
|
486
|
-
|
|
728
|
+
tag_id_str = f"[ {tag_id:20} ]"
|
|
729
|
+
logger.info(msg=f"{tag:25} {tag_id_str}: {data_str}")
|