shinestacker 1.9.1__py3-none-any.whl → 1.9.3__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/balance.py +1 -1
- shinestacker/algorithms/exif.py +689 -274
- shinestacker/algorithms/stack.py +7 -3
- shinestacker/gui/main_window.py +6 -0
- shinestacker/gui/menu_manager.py +8 -2
- shinestacker/gui/project_controller.py +1 -0
- shinestacker/retouch/brush_tool.py +20 -0
- shinestacker/retouch/exif_data.py +49 -4
- 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.3.dist-info}/METADATA +1 -1
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.3.dist-info}/RECORD +22 -22
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.3.dist-info}/WHEEL +0 -0
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.3.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.3.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.9.1.dist-info → shinestacker-1.9.3.dist-info}/top_level.txt +0 -0
shinestacker/algorithms/exif.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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
5
|
import traceback
|
|
5
6
|
import cv2
|
|
@@ -10,137 +11,461 @@ from PIL.PngImagePlugin import PngInfo
|
|
|
10
11
|
from PIL.ExifTags import TAGS
|
|
11
12
|
import tifffile
|
|
12
13
|
from .. config.constants import constants
|
|
13
|
-
from .utils import write_img, extension_jpg, extension_tif, extension_png
|
|
14
|
+
from .utils import read_img, write_img, extension_jpg, extension_tif, extension_png
|
|
14
15
|
|
|
16
|
+
# TIFF/EXIF Tag Constants
|
|
15
17
|
IMAGEWIDTH = 256
|
|
16
18
|
IMAGELENGTH = 257
|
|
17
|
-
RESOLUTIONX = 282
|
|
18
|
-
RESOLUTIONY = 283
|
|
19
|
-
RESOLUTIONUNIT = 296
|
|
20
19
|
BITSPERSAMPLE = 258
|
|
20
|
+
COMPRESSION = 259
|
|
21
21
|
PHOTOMETRICINTERPRETATION = 262
|
|
22
|
+
IMAGEDESCRIPTION = 270
|
|
23
|
+
MAKE = 271
|
|
24
|
+
MODEL = 272
|
|
25
|
+
STRIPOFFSETS = 273
|
|
26
|
+
ORIENTATION = 274
|
|
22
27
|
SAMPLESPERPIXEL = 277
|
|
28
|
+
ROWSPERSTRIP = 278
|
|
29
|
+
STRIPBYTECOUNTS = 279
|
|
30
|
+
XRESOLUTION = 282
|
|
31
|
+
YRESOLUTION = 283
|
|
23
32
|
PLANARCONFIGURATION = 284
|
|
33
|
+
RESOLUTIONUNIT = 296
|
|
24
34
|
SOFTWARE = 305
|
|
35
|
+
DATETIME = 306
|
|
36
|
+
ARTIST = 315
|
|
37
|
+
PREDICTOR = 317
|
|
38
|
+
WHITEPOINT = 318
|
|
39
|
+
PRIMARYCHROMATICITIES = 319
|
|
40
|
+
COLORMAP = 320
|
|
41
|
+
TILEWIDTH = 322
|
|
42
|
+
TILELENGTH = 323
|
|
43
|
+
TILEOFFSETS = 324
|
|
44
|
+
TILEBYTECOUNTS = 325
|
|
45
|
+
EXIFIFD = 34665
|
|
46
|
+
ICCPROFILE = 34675
|
|
47
|
+
COPYRIGHT = 33432
|
|
48
|
+
EXPOSURETIME = 33434
|
|
49
|
+
FNUMBER = 33437
|
|
50
|
+
EXPOSUREPROGRAM = 34850
|
|
51
|
+
ISOSPEEDRATINGS = 34855
|
|
52
|
+
EXIFVERSION = 36864
|
|
53
|
+
DATETIMEORIGINAL = 36867
|
|
54
|
+
DATETIMEDIGITIZED = 36868
|
|
55
|
+
SHUTTERSPEEDVALUE = 37377
|
|
56
|
+
APERTUREVALUE = 37378
|
|
57
|
+
BRIGHTNESSVALUE = 37379
|
|
58
|
+
EXPOSUREBIASVALUE = 37380
|
|
59
|
+
MAXAPERTUREVALUE = 37381
|
|
60
|
+
SUBJECTDISTANCE = 37382
|
|
61
|
+
METERINGMODE = 37383
|
|
62
|
+
LIGHTSOURCE = 37384
|
|
63
|
+
FLASH = 37385
|
|
64
|
+
FOCALLENGTH = 37386
|
|
65
|
+
MAKERNOTE = 37500
|
|
66
|
+
USERCOMMENT = 37510
|
|
67
|
+
SUBSECTIME = 37520
|
|
68
|
+
SUBSECTIMEORIGINAL = 37521
|
|
69
|
+
SUBSECTIMEDIGITIZED = 37522
|
|
70
|
+
FLASHPIXVERSION = 40960
|
|
71
|
+
COLORSPACE = 40961
|
|
72
|
+
PIXELXDIMENSION = 40962
|
|
73
|
+
PIXELYDIMENSION = 40963
|
|
74
|
+
RELATEDSOUNDFILE = 40964
|
|
75
|
+
FLASHENERGY = 41483
|
|
76
|
+
SPATIALFREQUENCYRESPONSE = 41484
|
|
77
|
+
FOCALPLANEXRESOLUTION = 41486
|
|
78
|
+
FOCALPLANEYRESOLUTION = 41487
|
|
79
|
+
FOCALPLANERESOLUTIONUNIT = 41488
|
|
80
|
+
SUBJECTLOCATION = 41492
|
|
81
|
+
EXPOSUREINDEX = 41493
|
|
82
|
+
SENSINGMETHOD = 41495
|
|
83
|
+
FILESOURCE = 41728
|
|
84
|
+
SCENETYPE = 41729
|
|
85
|
+
CFAPATTERN = 41730
|
|
86
|
+
CUSTOMRENDERED = 41985
|
|
87
|
+
EXPOSUREMODE = 41986
|
|
88
|
+
WHITEBALANCE = 41987
|
|
89
|
+
DIGITALZOOMRATIO = 41988
|
|
90
|
+
FOCALLENGTHIN35MMFILM = 41989
|
|
91
|
+
SCENECAPTURETYPE = 41990
|
|
92
|
+
GAINCONTROL = 41991
|
|
93
|
+
CONTRAST = 41992
|
|
94
|
+
SATURATION = 41993
|
|
95
|
+
SHARPNESS = 41994
|
|
96
|
+
DEVICESETTINGDESCRIPTION = 41995
|
|
97
|
+
SUBJECTDISTANCERANGE = 41996
|
|
98
|
+
IMAGEUNIQUEID = 42016
|
|
99
|
+
LENSINFO = 42034
|
|
100
|
+
LENSMAKE = 42035
|
|
101
|
+
LENSMODEL = 42036
|
|
102
|
+
GPSIFD = 34853
|
|
103
|
+
XMLPACKET = 700
|
|
25
104
|
IMAGERESOURCES = 34377
|
|
26
105
|
INTERCOLORPROFILE = 34675
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
106
|
+
|
|
107
|
+
NO_COPY_TIFF_TAGS_ID = [
|
|
108
|
+
IMAGEWIDTH, IMAGELENGTH, XRESOLUTION, YRESOLUTION, BITSPERSAMPLE,
|
|
109
|
+
PHOTOMETRICINTERPRETATION, SAMPLESPERPIXEL, PLANARCONFIGURATION, SOFTWARE,
|
|
110
|
+
RESOLUTIONUNIT, EXIFIFD, INTERCOLORPROFILE, IMAGERESOURCES,
|
|
111
|
+
STRIPOFFSETS, STRIPBYTECOUNTS, TILEOFFSETS, TILEBYTECOUNTS
|
|
112
|
+
]
|
|
113
|
+
|
|
34
114
|
NO_COPY_TIFF_TAGS = ["Compression", "StripOffsets", "RowsPerStrip", "StripByteCounts"]
|
|
35
115
|
|
|
116
|
+
XMP_TEMPLATE = """<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
|
117
|
+
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21'>
|
|
118
|
+
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
|
|
119
|
+
<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/'>
|
|
120
|
+
{content}
|
|
121
|
+
</rdf:Description>
|
|
122
|
+
</rdf:RDF>
|
|
123
|
+
</x:xmpmeta>
|
|
124
|
+
<?xpacket end='w'?>""" # noqa
|
|
125
|
+
|
|
126
|
+
XMP_EMPTY_TEMPLATE = """<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
|
127
|
+
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Adobe XMP Core 5.6-c140 79.160451, 2017/05/06-01:08:21'>
|
|
128
|
+
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
|
|
129
|
+
<rdf:Description rdf:about=''/>
|
|
130
|
+
</rdf:RDF>
|
|
131
|
+
</x:xmpmeta>
|
|
132
|
+
<?xpacket end='w'?>""" # noqa
|
|
133
|
+
|
|
134
|
+
XMP_TO_EXIF_MAP = {
|
|
135
|
+
'tiff:Make': MAKE,
|
|
136
|
+
'tiff:Model': MODEL,
|
|
137
|
+
'exif:ExposureTime': EXPOSURETIME,
|
|
138
|
+
'exif:FNumber': FNUMBER,
|
|
139
|
+
'exif:ISOSpeedRatings': ISOSPEEDRATINGS,
|
|
140
|
+
'exif:FocalLength': FOCALLENGTH,
|
|
141
|
+
'exif:DateTimeOriginal': DATETIMEORIGINAL,
|
|
142
|
+
'xmp:CreateDate': DATETIME,
|
|
143
|
+
'xmp:CreatorTool': SOFTWARE,
|
|
144
|
+
'aux:Lens': LENSMODEL, # Adobe's auxiliary namespace
|
|
145
|
+
'exifEX:LensModel': LENSMODEL, # EXIF 2.3 namespace
|
|
146
|
+
'exif:Flash': FLASH,
|
|
147
|
+
'exif:WhiteBalance': WHITEBALANCE,
|
|
148
|
+
'dc:description': IMAGEDESCRIPTION,
|
|
149
|
+
'dc:creator': ARTIST,
|
|
150
|
+
'dc:rights': COPYRIGHT,
|
|
151
|
+
'exif:ShutterSpeedValue': SHUTTERSPEEDVALUE,
|
|
152
|
+
'exif:ApertureValue': APERTUREVALUE,
|
|
153
|
+
'exif:ExposureBiasValue': EXPOSUREBIASVALUE,
|
|
154
|
+
'exif:MaxApertureValue': MAXAPERTUREVALUE,
|
|
155
|
+
'exif:MeteringMode': METERINGMODE,
|
|
156
|
+
'exif:ExposureMode': EXPOSUREMODE,
|
|
157
|
+
'exif:SceneCaptureType': SCENECAPTURETYPE
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
PNG_TAG_MAP = {
|
|
161
|
+
'EXIF_CameraMake': MAKE,
|
|
162
|
+
'EXIF_CameraModel': MODEL,
|
|
163
|
+
'EXIF_Software': SOFTWARE,
|
|
164
|
+
'EXIF_DateTime': DATETIME,
|
|
165
|
+
'EXIF_Artist': ARTIST,
|
|
166
|
+
'EXIF_Copyright': COPYRIGHT,
|
|
167
|
+
'EXIF_ExposureTime': EXPOSURETIME,
|
|
168
|
+
'EXIF_FNumber': FNUMBER,
|
|
169
|
+
'EXIF_ISOSpeed': ISOSPEEDRATINGS,
|
|
170
|
+
'EXIF_ShutterSpeedValue': SHUTTERSPEEDVALUE,
|
|
171
|
+
'EXIF_ApertureValue': APERTUREVALUE,
|
|
172
|
+
'EXIF_FocalLength': FOCALLENGTH,
|
|
173
|
+
'EXIF_LensModel': LENSMODEL,
|
|
174
|
+
'EXIF_ExposureBiasValue': EXPOSUREBIASVALUE,
|
|
175
|
+
'EXIF_MaxApertureValue': MAXAPERTUREVALUE,
|
|
176
|
+
'EXIF_MeteringMode': METERINGMODE,
|
|
177
|
+
'EXIF_Flash': FLASH,
|
|
178
|
+
'EXIF_WhiteBalance': WHITEBALANCE,
|
|
179
|
+
'EXIF_ExposureMode': EXPOSUREMODE,
|
|
180
|
+
'EXIF_SceneCaptureType': SCENECAPTURETYPE,
|
|
181
|
+
'EXIF_DateTimeOriginal': DATETIMEORIGINAL
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def safe_decode_bytes(data, encoding='utf-8'):
|
|
186
|
+
if not isinstance(data, bytes):
|
|
187
|
+
return data
|
|
188
|
+
encodings = [encoding, 'latin-1', 'cp1252', 'utf-16', 'ascii']
|
|
189
|
+
for enc in encodings:
|
|
190
|
+
try:
|
|
191
|
+
return data.decode(enc, errors='strict')
|
|
192
|
+
except UnicodeDecodeError:
|
|
193
|
+
continue
|
|
194
|
+
return data.decode('utf-8', errors='replace')
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
XMP_TAG_MAP = {
|
|
198
|
+
IMAGEDESCRIPTION: {'format': 'dc:description', 'type': 'rdf_alt',
|
|
199
|
+
'processor': safe_decode_bytes},
|
|
200
|
+
ARTIST: {'format': 'dc:creator', 'type': 'rdf_seq', 'processor': safe_decode_bytes},
|
|
201
|
+
COPYRIGHT: {'format': 'dc:rights', 'type': 'rdf_alt', 'processor': safe_decode_bytes},
|
|
202
|
+
MAKE: {'format': 'tiff:Make', 'type': 'simple', 'processor': safe_decode_bytes},
|
|
203
|
+
MODEL: {'format': 'tiff:Model', 'type': 'simple', 'processor': safe_decode_bytes},
|
|
204
|
+
DATETIME: {'format': 'xmp:CreateDate', 'type': 'datetime', 'processor': safe_decode_bytes},
|
|
205
|
+
DATETIMEORIGINAL: {'format': 'exif:DateTimeOriginal', 'type': 'datetime',
|
|
206
|
+
'processor': safe_decode_bytes},
|
|
207
|
+
SOFTWARE: {'format': 'xmp:CreatorTool', 'type': 'simple', 'processor': safe_decode_bytes},
|
|
208
|
+
EXPOSURETIME: {'format': 'exif:ExposureTime', 'type': 'rational', 'processor': None},
|
|
209
|
+
FNUMBER: {'format': 'exif:FNumber', 'type': 'rational', 'processor': None},
|
|
210
|
+
ISOSPEEDRATINGS: {'format': 'exif:ISOSpeedRatings', 'type': 'rdf_seq', 'processor': None},
|
|
211
|
+
FOCALLENGTH: {'format': 'exif:FocalLength', 'type': 'rational', 'processor': None},
|
|
212
|
+
LENSMODEL: {'format': 'aux:Lens', 'type': 'simple', 'processor': safe_decode_bytes},
|
|
213
|
+
SHUTTERSPEEDVALUE: {'format': 'exif:ShutterSpeedValue', 'type': 'rational', 'processor': None},
|
|
214
|
+
APERTUREVALUE: {'format': 'exif:ApertureValue', 'type': 'rational', 'processor': None},
|
|
215
|
+
EXPOSUREBIASVALUE: {'format': 'exif:ExposureBiasValue', 'type': 'rational', 'processor': None},
|
|
216
|
+
MAXAPERTUREVALUE: {'format': 'exif:MaxApertureValue', 'type': 'rational', 'processor': None},
|
|
217
|
+
METERINGMODE: {'format': 'exif:MeteringMode', 'type': 'simple', 'processor': None},
|
|
218
|
+
FLASH: {'format': 'exif:Flash', 'type': 'simple', 'processor': None},
|
|
219
|
+
WHITEBALANCE: {'format': 'exif:WhiteBalance', 'type': 'mapped', 'processor': None,
|
|
220
|
+
'map': {0: 'Auto', 1: 'Manual'}},
|
|
221
|
+
EXPOSUREMODE: {'format': 'exif:ExposureMode', 'type': 'mapped', 'processor': None,
|
|
222
|
+
'map': {0: 'Auto', 1: 'Manual', 2: 'Auto bracket'}},
|
|
223
|
+
SCENECAPTURETYPE: {'format': 'exif:SceneCaptureType', 'type': 'mapped', 'processor': None,
|
|
224
|
+
'map': {0: 'Standard', 1: 'Landscape', 2: 'Portrait', 3: 'Night scene'}}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
CAMERA_TAGS_MAP = {
|
|
228
|
+
MAKE: 'CameraMake',
|
|
229
|
+
MODEL: 'CameraModel',
|
|
230
|
+
SOFTWARE: 'Software',
|
|
231
|
+
DATETIME: 'DateTime',
|
|
232
|
+
ARTIST: 'Artist',
|
|
233
|
+
COPYRIGHT: 'Copyright'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
EXPOSURE_TAGS_MAP = {
|
|
237
|
+
EXPOSURETIME: 'ExposureTime',
|
|
238
|
+
FNUMBER: 'FNumber',
|
|
239
|
+
ISOSPEEDRATINGS: 'ISOSpeed',
|
|
240
|
+
SHUTTERSPEEDVALUE: 'ShutterSpeedValue',
|
|
241
|
+
APERTUREVALUE: 'ApertureValue',
|
|
242
|
+
FOCALLENGTH: 'FocalLength',
|
|
243
|
+
LENSMODEL: 'LensModel',
|
|
244
|
+
EXPOSUREBIASVALUE: 'ExposureBiasValue',
|
|
245
|
+
MAXAPERTUREVALUE: 'MaxApertureValue',
|
|
246
|
+
METERINGMODE: 'MeteringMode',
|
|
247
|
+
FLASH: 'Flash',
|
|
248
|
+
WHITEBALANCE: 'WhiteBalance',
|
|
249
|
+
EXPOSUREMODE: 'ExposureMode',
|
|
250
|
+
SCENECAPTURETYPE: 'SceneCaptureType',
|
|
251
|
+
DATETIMEORIGINAL: 'DateTimeOriginal'
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
EXPOSURE_DATA_TIFF = {
|
|
255
|
+
'ExposureTime': EXPOSURETIME,
|
|
256
|
+
'FNumber': FNUMBER,
|
|
257
|
+
'ISOSpeedRatings': ISOSPEEDRATINGS,
|
|
258
|
+
'FocalLength': FOCALLENGTH,
|
|
259
|
+
'LensModel': LENSMODEL,
|
|
260
|
+
'ShutterSpeedValue': SHUTTERSPEEDVALUE,
|
|
261
|
+
'ApertureValue': APERTUREVALUE,
|
|
262
|
+
'ExposureBiasValue': EXPOSUREBIASVALUE,
|
|
263
|
+
'MaxApertureValue': MAXAPERTUREVALUE,
|
|
264
|
+
'MeteringMode': METERINGMODE,
|
|
265
|
+
'Flash': FLASH,
|
|
266
|
+
'WhiteBalance': WHITEBALANCE,
|
|
267
|
+
'ExposureMode': EXPOSUREMODE,
|
|
268
|
+
'SceneCaptureType': SCENECAPTURETYPE,
|
|
269
|
+
'DateTimeOriginal': DATETIMEORIGINAL,
|
|
270
|
+
'Make': MAKE,
|
|
271
|
+
'Model': MODEL
|
|
272
|
+
}
|
|
273
|
+
|
|
36
274
|
|
|
37
275
|
def extract_enclosed_data_for_jpg(data, head, foot):
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if xmp_end == -1:
|
|
44
|
-
return None
|
|
45
|
-
xmp_end += len(foot)
|
|
46
|
-
return data[xmp_start:xmp_end]
|
|
47
|
-
except Exception:
|
|
276
|
+
xmp_start = data.find(head)
|
|
277
|
+
if xmp_start == -1:
|
|
278
|
+
return None
|
|
279
|
+
xmp_end = data.find(foot, xmp_start)
|
|
280
|
+
if xmp_end == -1:
|
|
48
281
|
return None
|
|
282
|
+
xmp_end += len(foot)
|
|
283
|
+
return data[xmp_start:xmp_end]
|
|
49
284
|
|
|
50
285
|
|
|
51
|
-
def get_exif(exif_filename):
|
|
286
|
+
def get_exif(exif_filename, enhanced_png_parsing=True):
|
|
52
287
|
if not os.path.isfile(exif_filename):
|
|
53
288
|
raise RuntimeError(f"File does not exist: {exif_filename}")
|
|
54
289
|
image = Image.open(exif_filename)
|
|
55
290
|
if extension_tif(exif_filename):
|
|
56
|
-
return
|
|
291
|
+
return get_exif_from_tiff(image, exif_filename)
|
|
57
292
|
if extension_jpg(exif_filename):
|
|
58
|
-
|
|
59
|
-
with open(exif_filename, 'rb') as f:
|
|
60
|
-
data = extract_enclosed_data_for_jpg(f.read(), b'<?xpacket', b'<?xpacket end="w"?>')
|
|
61
|
-
if data is not None:
|
|
62
|
-
exif_data[XMLPACKET] = data
|
|
63
|
-
return exif_data
|
|
293
|
+
return get_exif_from_jpg(image, exif_filename)
|
|
64
294
|
if extension_png(exif_filename):
|
|
295
|
+
if enhanced_png_parsing:
|
|
296
|
+
return get_enhanced_exif_from_png(image)
|
|
65
297
|
exif_data = get_exif_from_png(image)
|
|
66
298
|
return exif_data if exif_data else image.getexif()
|
|
67
299
|
return image.getexif()
|
|
68
300
|
|
|
69
301
|
|
|
70
|
-
def
|
|
71
|
-
exif_data =
|
|
302
|
+
def get_exif_from_tiff(image, exif_filename):
|
|
303
|
+
exif_data = image.tag_v2 if hasattr(image, 'tag_v2') else image.getexif()
|
|
72
304
|
try:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
305
|
+
with tifffile.TiffFile(exif_filename) as tif:
|
|
306
|
+
for page in tif.pages:
|
|
307
|
+
if EXIFIFD in page.tags:
|
|
308
|
+
exif_dict_data = page.tags[EXIFIFD].value
|
|
309
|
+
for exif_key, tag_id in EXPOSURE_DATA_TIFF.items():
|
|
310
|
+
if exif_key in exif_dict_data:
|
|
311
|
+
value = exif_dict_data[exif_key]
|
|
312
|
+
if isinstance(value, tuple) and len(value) == 2:
|
|
313
|
+
value = IFDRational(value[0], value[1])
|
|
314
|
+
exif_data[tag_id] = value
|
|
315
|
+
break
|
|
316
|
+
except Exception as e:
|
|
317
|
+
print(f"Error reading EXIF with tifffile: {e}")
|
|
78
318
|
try:
|
|
79
|
-
if
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
319
|
+
if XMLPACKET in exif_data:
|
|
320
|
+
xmp_data = exif_data[XMLPACKET]
|
|
321
|
+
if isinstance(xmp_data, bytes):
|
|
322
|
+
xmp_string = xmp_data.decode('utf-8', errors='ignore')
|
|
323
|
+
else:
|
|
324
|
+
xmp_string = str(xmp_data)
|
|
325
|
+
xmp_exif = parse_xmp_to_exif(xmp_string)
|
|
326
|
+
for tag_id in [EXPOSURETIME, FNUMBER, ISOSPEEDRATINGS, FOCALLENGTH, LENSMODEL]:
|
|
327
|
+
if tag_id in xmp_exif and tag_id not in exif_data:
|
|
328
|
+
exif_data[tag_id] = xmp_exif[tag_id]
|
|
329
|
+
except Exception as e:
|
|
330
|
+
print(f"Error processing XMP: {e}")
|
|
331
|
+
return exif_data
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_exif_from_jpg(image, exif_filename):
|
|
335
|
+
exif_data = image.getexif()
|
|
336
|
+
try:
|
|
337
|
+
exif_subifd = exif_data.get_ifd(EXIFIFD)
|
|
338
|
+
for tag_id, value in exif_subifd.items():
|
|
339
|
+
if tag_id in EXPOSURE_TAGS_MAP:
|
|
340
|
+
exif_data[tag_id] = value
|
|
341
|
+
elif tag_id not in exif_data:
|
|
342
|
+
exif_data[tag_id] = value
|
|
86
343
|
except Exception:
|
|
87
344
|
pass
|
|
345
|
+
if MAKERNOTE in exif_data:
|
|
346
|
+
del exif_data[MAKERNOTE]
|
|
347
|
+
with open(exif_filename, 'rb') as f:
|
|
348
|
+
data = extract_enclosed_data_for_jpg(f.read(), b'<?xpacket', b'<?xpacket end="w"?>')
|
|
349
|
+
if data is not None:
|
|
350
|
+
exif_data[XMLPACKET] = data
|
|
88
351
|
return exif_data
|
|
89
352
|
|
|
90
353
|
|
|
91
|
-
def
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
354
|
+
def get_exif_from_png(image):
|
|
355
|
+
exif_data = {}
|
|
356
|
+
exif_from_image = image.getexif()
|
|
357
|
+
if exif_from_image:
|
|
358
|
+
exif_data.update(dict(exif_from_image))
|
|
359
|
+
for attr_name in ['text', 'info']:
|
|
360
|
+
if hasattr(image, attr_name) and getattr(image, attr_name):
|
|
361
|
+
for key, value in getattr(image, attr_name).items():
|
|
362
|
+
if attr_name == 'info' and key in ['dpi', 'gamma']:
|
|
363
|
+
continue
|
|
364
|
+
exif_data[f"PNG_{key}"] = value
|
|
365
|
+
return exif_data
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def parse_xmp_to_exif(xmp_data):
|
|
369
|
+
exif_data = {}
|
|
370
|
+
if not xmp_data:
|
|
371
|
+
return exif_data
|
|
372
|
+
if isinstance(xmp_data, bytes):
|
|
373
|
+
xmp_data = xmp_data.decode('utf-8', errors='ignore')
|
|
374
|
+
for xmp_tag, exif_tag in XMP_TO_EXIF_MAP.items():
|
|
375
|
+
attr_pattern = f'{xmp_tag}="([^"]*)"'
|
|
376
|
+
attr_matches = re.findall(attr_pattern, xmp_data)
|
|
377
|
+
for value in attr_matches:
|
|
378
|
+
if value:
|
|
379
|
+
exif_data[exif_tag] = _parse_xmp_value(exif_tag, value)
|
|
380
|
+
start_tag = f'<{xmp_tag}>'
|
|
381
|
+
end_tag = f'</{xmp_tag}>'
|
|
382
|
+
if start_tag in xmp_data:
|
|
383
|
+
start = xmp_data.find(start_tag) + len(start_tag)
|
|
384
|
+
end = xmp_data.find(end_tag, start)
|
|
385
|
+
if end != -1:
|
|
386
|
+
value = xmp_data[start:end].strip()
|
|
387
|
+
if value:
|
|
388
|
+
exif_data[exif_tag] = _parse_xmp_value(exif_tag, value)
|
|
389
|
+
return exif_data
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _parse_xmp_value(exif_tag, value):
|
|
393
|
+
if exif_tag in [EXPOSURETIME, FNUMBER, FOCALLENGTH]:
|
|
394
|
+
if '/' in value:
|
|
395
|
+
num, den = value.split('/')
|
|
396
|
+
try:
|
|
397
|
+
return IFDRational(int(num), int(den))
|
|
398
|
+
except (ValueError, ZeroDivisionError):
|
|
399
|
+
try:
|
|
400
|
+
return float(value) if value else 0.0
|
|
401
|
+
except ValueError:
|
|
402
|
+
return 0.0 # Return 0.0 if float conversion also fails
|
|
403
|
+
return float(value) if value else 0.0
|
|
404
|
+
if exif_tag == ISOSPEEDRATINGS: # ISO
|
|
405
|
+
if '<rdf:li>' in value:
|
|
406
|
+
matches = re.findall(r'<rdf:li>([^<]+)</rdf:li>', value)
|
|
407
|
+
if matches:
|
|
408
|
+
value = matches[0]
|
|
96
409
|
try:
|
|
97
|
-
return
|
|
98
|
-
except
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
410
|
+
return int(value)
|
|
411
|
+
except ValueError:
|
|
412
|
+
return value
|
|
413
|
+
if exif_tag in [DATETIME, DATETIMEORIGINAL]: # DateTime and DateTimeOriginal
|
|
414
|
+
if 'T' in value:
|
|
415
|
+
value = value.replace('T', ' ').replace('-', ':')
|
|
416
|
+
return value
|
|
417
|
+
return value
|
|
104
418
|
|
|
105
419
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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):
|
|
420
|
+
def parse_typed_png_text(value):
|
|
421
|
+
if isinstance(value, str):
|
|
422
|
+
if value.startswith('RATIONAL:'):
|
|
423
|
+
parts = value[9:].split('/')
|
|
424
|
+
if len(parts) == 2:
|
|
425
|
+
try:
|
|
426
|
+
return IFDRational(int(parts[0]), int(parts[1]))
|
|
427
|
+
except (ValueError, ZeroDivisionError):
|
|
428
|
+
return value # Return original value if parsing fails
|
|
429
|
+
elif value.startswith('INT:'):
|
|
123
430
|
try:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
431
|
+
return int(value[4:])
|
|
432
|
+
except ValueError:
|
|
433
|
+
return value[4:] # Return string part if int conversion fails
|
|
434
|
+
elif value.startswith('FLOAT:'):
|
|
435
|
+
try:
|
|
436
|
+
return float(value[6:])
|
|
437
|
+
except ValueError:
|
|
438
|
+
return value[6:] # Return string part if float conversion fails
|
|
439
|
+
elif value.startswith('STRING:'):
|
|
440
|
+
return value[7:]
|
|
441
|
+
elif value.startswith('BYTES:'):
|
|
442
|
+
return value[6:].encode('utf-8')
|
|
443
|
+
elif value.startswith('ARRAY:'):
|
|
444
|
+
return [x.strip() for x in value[6:].split(',')]
|
|
445
|
+
return value
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def get_enhanced_exif_from_png(image):
|
|
449
|
+
basic_exif = get_exif_from_png(image)
|
|
450
|
+
enhanced_exif = {}
|
|
451
|
+
enhanced_exif.update(basic_exif)
|
|
452
|
+
xmp_data = None
|
|
453
|
+
if hasattr(image, 'text') and image.text:
|
|
454
|
+
xmp_data = image.text.get('XML:com.adobe.xmp') or image.text.get('xml:com.adobe.xmp')
|
|
455
|
+
if not xmp_data and XMLPACKET in basic_exif:
|
|
456
|
+
xmp_data = basic_exif[XMLPACKET]
|
|
457
|
+
if xmp_data:
|
|
458
|
+
enhanced_exif.update(parse_xmp_to_exif(xmp_data))
|
|
459
|
+
if hasattr(image, 'text') and image.text:
|
|
460
|
+
for key, value in image.text.items():
|
|
461
|
+
if key.startswith('EXIF_'):
|
|
462
|
+
parsed_value = parse_typed_png_text(value)
|
|
463
|
+
tag_id = PNG_TAG_MAP.get(key)
|
|
464
|
+
if tag_id:
|
|
465
|
+
enhanced_exif[tag_id] = parsed_value
|
|
466
|
+
if MAKERNOTE in enhanced_exif:
|
|
467
|
+
del enhanced_exif[MAKERNOTE]
|
|
468
|
+
return {k: v for k, v in enhanced_exif.items() if isinstance(k, int)}
|
|
144
469
|
|
|
145
470
|
|
|
146
471
|
def get_tiff_dtype_count(value):
|
|
@@ -169,36 +494,93 @@ def get_tiff_dtype_count(value):
|
|
|
169
494
|
return 4, 1 # uint32
|
|
170
495
|
if isinstance(value, float):
|
|
171
496
|
return 11, 1 # float64
|
|
172
|
-
return 2, len(str(value)) + 1 # Default for
|
|
497
|
+
return 2, len(str(value)) + 1 # Default for other cases (ASCII string)
|
|
173
498
|
|
|
174
499
|
|
|
175
500
|
def add_exif_data_to_jpg_file(exif, in_filename, out_filename, verbose=False):
|
|
176
|
-
logger = logging.getLogger(__name__)
|
|
177
501
|
if exif is None:
|
|
178
502
|
raise RuntimeError('No exif data provided.')
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
503
|
+
logger = logging.getLogger(__name__)
|
|
504
|
+
xmp_data = exif.get(XMLPACKET) if hasattr(exif, 'get') else None
|
|
505
|
+
if out_filename is None:
|
|
506
|
+
out_filename = in_filename
|
|
507
|
+
use_temp = in_filename == out_filename
|
|
508
|
+
if use_temp:
|
|
509
|
+
temp_filename = out_filename + ".tmp"
|
|
510
|
+
final_filename = temp_filename
|
|
511
|
+
else:
|
|
512
|
+
final_filename = out_filename
|
|
513
|
+
try:
|
|
514
|
+
with Image.open(in_filename) as image:
|
|
515
|
+
jpeg_exif = Image.Exif()
|
|
516
|
+
compatible_tags = [
|
|
517
|
+
MAKE, MODEL, SOFTWARE, DATETIME, ARTIST, COPYRIGHT,
|
|
518
|
+
EXPOSURETIME, FNUMBER, ISOSPEEDRATINGS, EXPOSUREPROGRAM,
|
|
519
|
+
SHUTTERSPEEDVALUE, APERTUREVALUE, BRIGHTNESSVALUE, EXPOSUREBIASVALUE,
|
|
520
|
+
MAXAPERTUREVALUE, SUBJECTDISTANCE, METERINGMODE, LIGHTSOURCE, FLASH,
|
|
521
|
+
FOCALLENGTH, EXPOSUREMODE, WHITEBALANCE, EXPOSUREINDEX,
|
|
522
|
+
SCENECAPTURETYPE, DATETIMEORIGINAL, LENSMODEL, LENSMAKE,
|
|
523
|
+
FOCALLENGTHIN35MMFILM, GAINCONTROL, CONTRAST, SATURATION, SHARPNESS,
|
|
524
|
+
CUSTOMRENDERED, DIGITALZOOMRATIO, SUBJECTDISTANCERANGE,
|
|
525
|
+
EXIFVERSION, FLASHPIXVERSION,
|
|
526
|
+
COLORSPACE, PIXELXDIMENSION, PIXELYDIMENSION, IMAGEWIDTH, IMAGELENGTH,
|
|
527
|
+
BITSPERSAMPLE, ORIENTATION, XRESOLUTION, YRESOLUTION, RESOLUTIONUNIT
|
|
528
|
+
]
|
|
529
|
+
for tag_id in compatible_tags:
|
|
530
|
+
if tag_id in exif:
|
|
531
|
+
value = exif[tag_id]
|
|
532
|
+
try:
|
|
533
|
+
if tag_id in [EXIFVERSION, FLASHPIXVERSION]:
|
|
534
|
+
if isinstance(value, str):
|
|
535
|
+
jpeg_exif[tag_id] = value.encode('ascii')
|
|
536
|
+
else:
|
|
537
|
+
jpeg_exif[tag_id] = value
|
|
538
|
+
elif isinstance(value, tuple) and len(value) == 2:
|
|
539
|
+
value = IFDRational(value[0], value[1])
|
|
540
|
+
jpeg_exif[tag_id] = value
|
|
541
|
+
elif isinstance(value, (int, str, float, IFDRational)):
|
|
542
|
+
jpeg_exif[tag_id] = value
|
|
543
|
+
except Exception as e:
|
|
544
|
+
if verbose:
|
|
545
|
+
logger.warning(msg=f"Failed to add tag {tag_id}: {e}")
|
|
197
546
|
try:
|
|
198
|
-
|
|
547
|
+
if hasattr(jpeg_exif, 'get_ifd'):
|
|
548
|
+
exif_ifd = jpeg_exif.get_ifd(EXIFIFD)
|
|
549
|
+
if exif_ifd is None:
|
|
550
|
+
exif_ifd = {}
|
|
551
|
+
tags_to_move = [
|
|
552
|
+
LENSMODEL, EXPOSURETIME, FNUMBER, ISOSPEEDRATINGS, FOCALLENGTH,
|
|
553
|
+
SHUTTERSPEEDVALUE, APERTUREVALUE, EXPOSUREBIASVALUE,
|
|
554
|
+
]
|
|
555
|
+
for tag_id in tags_to_move:
|
|
556
|
+
if tag_id in exif:
|
|
557
|
+
exif_ifd[tag_id] = exif[tag_id]
|
|
558
|
+
if tag_id in jpeg_exif:
|
|
559
|
+
del jpeg_exif[tag_id]
|
|
199
560
|
except Exception as e:
|
|
200
561
|
if verbose:
|
|
201
|
-
logger.warning(msg=f"Failed to
|
|
562
|
+
logger.warning(msg=f"Failed to move tags to EXIF sub-IFD: {e}")
|
|
563
|
+
exif_bytes = jpeg_exif.tobytes()
|
|
564
|
+
image.save(final_filename, "JPEG", exif=exif_bytes, quality=100)
|
|
565
|
+
if xmp_data and isinstance(xmp_data, bytes):
|
|
566
|
+
_insert_xmp_into_jpeg(final_filename, xmp_data, verbose)
|
|
567
|
+
if use_temp:
|
|
568
|
+
if os.path.exists(out_filename):
|
|
569
|
+
os.remove(out_filename)
|
|
570
|
+
os.rename(temp_filename, out_filename)
|
|
571
|
+
except Exception as e:
|
|
572
|
+
traceback.print_tb(e.__traceback__)
|
|
573
|
+
if use_temp and os.path.exists(temp_filename):
|
|
574
|
+
try:
|
|
575
|
+
os.remove(temp_filename)
|
|
576
|
+
except Exception as ee:
|
|
577
|
+
traceback.print_tb(ee.__traceback__)
|
|
578
|
+
else:
|
|
579
|
+
try:
|
|
580
|
+
write_img(out_filename, read_img(in_filename))
|
|
581
|
+
except Exception as ee:
|
|
582
|
+
traceback.print_tb(ee.__traceback__)
|
|
583
|
+
raise
|
|
202
584
|
|
|
203
585
|
|
|
204
586
|
def _insert_xmp_into_jpeg(jpeg_path, xmp_data, verbose=False):
|
|
@@ -243,75 +625,41 @@ def create_xmp_from_exif(exif_data):
|
|
|
243
625
|
xmp_elements = []
|
|
244
626
|
if exif_data:
|
|
245
627
|
for tag_id, value in exif_data.items():
|
|
246
|
-
if isinstance(tag_id, int):
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
628
|
+
if isinstance(tag_id, int) and value and tag_id in XMP_TAG_MAP:
|
|
629
|
+
config = XMP_TAG_MAP[tag_id]
|
|
630
|
+
processed_value = config['processor'](value) if config['processor'] else value
|
|
631
|
+
if config['type'] == 'simple':
|
|
632
|
+
xmp_elements.append(
|
|
633
|
+
f'<{config["format"]}>{processed_value}</{config["format"]}>')
|
|
634
|
+
elif config['type'] == 'rdf_alt':
|
|
635
|
+
xmp_elements.append(
|
|
636
|
+
f'<{config["format"]}><rdf:Alt>'
|
|
637
|
+
f'<rdf:li xml:lang="x-default">{processed_value}</rdf:li>'
|
|
638
|
+
f'</rdf:Alt></{config["format"]}>')
|
|
639
|
+
elif config['type'] == 'rdf_seq':
|
|
251
640
|
xmp_elements.append(
|
|
252
|
-
f'<
|
|
253
|
-
'</rdf:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if
|
|
257
|
-
|
|
641
|
+
f'<{config["format"]}><rdf:Seq>'
|
|
642
|
+
f'<rdf:li>{processed_value}</rdf:li>'
|
|
643
|
+
f'</rdf:Seq></{config["format"]}>')
|
|
644
|
+
elif config['type'] == 'datetime':
|
|
645
|
+
if ':' in processed_value:
|
|
646
|
+
processed_value = processed_value.replace(':', '-', 2).replace(' ', 'T')
|
|
258
647
|
xmp_elements.append(
|
|
259
|
-
f'<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
copyright_tag = copyright_tag.decode('utf-8', errors='ignore')
|
|
648
|
+
f'<{config["format"]}>{processed_value}</{config["format"]}>')
|
|
649
|
+
elif config['type'] == 'rational':
|
|
650
|
+
float_value = float(value) \
|
|
651
|
+
if hasattr(value, 'numerator') \
|
|
652
|
+
else (float(value) if value else 0)
|
|
265
653
|
xmp_elements.append(
|
|
266
|
-
f'<
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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>')
|
|
654
|
+
f'<{config["format"]}>{float_value}</{config["format"]}>')
|
|
655
|
+
elif config['type'] == 'mapped':
|
|
656
|
+
mapped_value = config['map'].get(value, str(value))
|
|
657
|
+
xmp_elements.append(
|
|
658
|
+
f'<{config["format"]}>{mapped_value}</{config["format"]}>')
|
|
290
659
|
if xmp_elements:
|
|
291
660
|
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'?>"""
|
|
661
|
+
return XMP_TEMPLATE.format(content=xmp_content)
|
|
662
|
+
return XMP_EMPTY_TEMPLATE
|
|
315
663
|
|
|
316
664
|
|
|
317
665
|
def write_image_with_exif_data_png(exif, image, out_filename, verbose=False, color_order='auto'):
|
|
@@ -321,83 +669,81 @@ def write_image_with_exif_data_png(exif, image, out_filename, verbose=False, col
|
|
|
321
669
|
logger.warning(msg="EXIF data not supported for 16-bit PNG format")
|
|
322
670
|
write_img(out_filename, image)
|
|
323
671
|
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')
|
|
672
|
+
pil_image = _convert_to_pil_image(image, color_order)
|
|
673
|
+
pnginfo, icc_profile = _prepare_png_metadata(exif)
|
|
674
|
+
save_args = {'format': 'PNG', 'pnginfo': pnginfo}
|
|
675
|
+
if icc_profile:
|
|
676
|
+
save_args['icc_profile'] = icc_profile
|
|
677
|
+
pil_image.save(out_filename, **save_args)
|
|
343
678
|
|
|
344
679
|
|
|
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
|
|
680
|
+
def _convert_to_pil_image(image, color_order):
|
|
681
|
+
if isinstance(image, np.ndarray) and len(image.shape) == 3 and image.shape[2] == 3:
|
|
682
|
+
if color_order in ['auto', 'bgr']:
|
|
683
|
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
684
|
+
return Image.fromarray(image_rgb)
|
|
685
|
+
return Image.fromarray(image) if isinstance(image, np.ndarray) else image
|
|
355
686
|
|
|
356
687
|
|
|
357
|
-
def _prepare_png_metadata(exif
|
|
688
|
+
def _prepare_png_metadata(exif):
|
|
358
689
|
pnginfo = PngInfo()
|
|
359
690
|
icc_profile = None
|
|
360
|
-
xmp_data = _extract_xmp_data(exif
|
|
691
|
+
xmp_data = _extract_xmp_data(exif)
|
|
361
692
|
if xmp_data:
|
|
362
693
|
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)
|
|
694
|
+
_add_exif_tags_to_pnginfo(exif, pnginfo)
|
|
695
|
+
icc_profile = _extract_icc_profile(exif)
|
|
367
696
|
return pnginfo, icc_profile
|
|
368
697
|
|
|
369
698
|
|
|
370
|
-
def _extract_xmp_data(exif
|
|
699
|
+
def _extract_xmp_data(exif):
|
|
371
700
|
for key, value in exif.items():
|
|
372
701
|
if isinstance(key, str) and ('xmp' in key.lower() or 'xml' in key.lower()):
|
|
373
702
|
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}")
|
|
703
|
+
return value.decode('utf-8', errors='ignore')
|
|
704
|
+
if isinstance(value, str):
|
|
384
705
|
return value
|
|
385
|
-
if verbose:
|
|
386
|
-
logger.info("Generated new XMP data from EXIF")
|
|
387
706
|
return create_xmp_from_exif(exif)
|
|
388
707
|
|
|
389
708
|
|
|
390
|
-
def _add_exif_tags_to_pnginfo(exif, pnginfo
|
|
709
|
+
def _add_exif_tags_to_pnginfo(exif, pnginfo):
|
|
391
710
|
for tag_id, value in exif.items():
|
|
392
711
|
if value is None:
|
|
393
712
|
continue
|
|
394
713
|
if isinstance(tag_id, int):
|
|
395
|
-
|
|
714
|
+
if tag_id in CAMERA_TAGS_MAP:
|
|
715
|
+
_add_typed_tag(pnginfo, f"EXIF_{CAMERA_TAGS_MAP[tag_id]}", value)
|
|
716
|
+
elif tag_id in EXPOSURE_TAGS_MAP:
|
|
717
|
+
_add_typed_tag(pnginfo, f"EXIF_{EXPOSURE_TAGS_MAP[tag_id]}", value)
|
|
718
|
+
else:
|
|
719
|
+
_add_exif_tag(pnginfo, tag_id, value)
|
|
396
720
|
elif isinstance(tag_id, str) and not tag_id.lower().startswith(('xmp', 'xml')):
|
|
397
|
-
_add_png_text_tag(pnginfo, tag_id, value
|
|
721
|
+
_add_png_text_tag(pnginfo, tag_id, value)
|
|
398
722
|
|
|
399
723
|
|
|
400
|
-
def
|
|
724
|
+
def _add_typed_tag(pnginfo, key, value):
|
|
725
|
+
try:
|
|
726
|
+
if hasattr(value, 'numerator'):
|
|
727
|
+
stored_value = f"RATIONAL:{value.numerator}/{value.denominator}"
|
|
728
|
+
elif isinstance(value, bytes):
|
|
729
|
+
try:
|
|
730
|
+
stored_value = f"STRING:{value.decode('utf-8', errors='replace')}"
|
|
731
|
+
except Exception:
|
|
732
|
+
stored_value = f"BYTES:{str(value)[:100]}"
|
|
733
|
+
elif isinstance(value, (list, tuple)):
|
|
734
|
+
stored_value = f"ARRAY:{','.join(str(x) for x in value)}"
|
|
735
|
+
elif isinstance(value, int):
|
|
736
|
+
stored_value = f"INT:{value}"
|
|
737
|
+
elif isinstance(value, float):
|
|
738
|
+
stored_value = f"FLOAT:{value}"
|
|
739
|
+
else:
|
|
740
|
+
stored_value = f"STRING:{str(value)}"
|
|
741
|
+
pnginfo.add_text(key, stored_value)
|
|
742
|
+
except Exception:
|
|
743
|
+
pass
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _add_exif_tag(pnginfo, tag_id, value):
|
|
401
747
|
try:
|
|
402
748
|
tag_name = TAGS.get(tag_id, f"Unknown_{tag_id}")
|
|
403
749
|
if isinstance(value, bytes) and len(value) > 1000:
|
|
@@ -410,17 +756,16 @@ def _add_exif_tag(pnginfo, tag_id, value, verbose, logger):
|
|
|
410
756
|
pnginfo.add_text(tag_name, decoded_value)
|
|
411
757
|
except Exception:
|
|
412
758
|
pass
|
|
413
|
-
elif hasattr(value, 'numerator'):
|
|
759
|
+
elif hasattr(value, 'numerator'):
|
|
414
760
|
rational_str = f"{value.numerator}/{value.denominator}"
|
|
415
761
|
pnginfo.add_text(tag_name, rational_str)
|
|
416
762
|
else:
|
|
417
763
|
pnginfo.add_text(tag_name, str(value))
|
|
418
|
-
except Exception
|
|
419
|
-
|
|
420
|
-
logger.warning(f"Could not store EXIF tag {tag_id}: {e}")
|
|
764
|
+
except Exception:
|
|
765
|
+
pass
|
|
421
766
|
|
|
422
767
|
|
|
423
|
-
def _add_png_text_tag(pnginfo, key, value
|
|
768
|
+
def _add_png_text_tag(pnginfo, key, value):
|
|
424
769
|
try:
|
|
425
770
|
clean_key = key[4:] if key.startswith('PNG_') else key
|
|
426
771
|
if 'icc' in clean_key.lower() or 'profile' in clean_key.lower():
|
|
@@ -434,33 +779,98 @@ def _add_png_text_tag(pnginfo, key, value, verbose, logger):
|
|
|
434
779
|
pnginfo.add_text(clean_key, truncated_value)
|
|
435
780
|
else:
|
|
436
781
|
pnginfo.add_text(clean_key, str(value))
|
|
437
|
-
except Exception
|
|
438
|
-
|
|
439
|
-
logger.warning(msg=f"Could not store PNG metadata {key}: {e}")
|
|
782
|
+
except Exception:
|
|
783
|
+
pass
|
|
440
784
|
|
|
441
785
|
|
|
442
|
-
def _extract_icc_profile(exif
|
|
786
|
+
def _extract_icc_profile(exif):
|
|
443
787
|
for key, value in exif.items():
|
|
444
788
|
if (isinstance(key, str) and
|
|
445
789
|
isinstance(value, bytes) and
|
|
446
790
|
('icc' in key.lower() or 'profile' in key.lower())):
|
|
447
|
-
if verbose:
|
|
448
|
-
logger.info(f"Found ICC profile: {key}")
|
|
449
791
|
return value
|
|
450
792
|
return None
|
|
451
793
|
|
|
452
794
|
|
|
795
|
+
def clean_data_for_tiff(data):
|
|
796
|
+
if isinstance(data, str):
|
|
797
|
+
return data.encode('ascii', 'ignore').decode('ascii')
|
|
798
|
+
if isinstance(data, bytes):
|
|
799
|
+
decoded = data.decode('utf-8', 'ignore')
|
|
800
|
+
return decoded.encode('ascii', 'ignore').decode('ascii')
|
|
801
|
+
if isinstance(data, IFDRational):
|
|
802
|
+
return (data.numerator, data.denominator)
|
|
803
|
+
return data
|
|
804
|
+
|
|
805
|
+
|
|
453
806
|
def write_image_with_exif_data_jpg(exif, image, out_filename, verbose):
|
|
454
|
-
|
|
807
|
+
save_img = (image // 256).astype(np.uint8) if image.dtype == np.uint16 else image
|
|
808
|
+
cv2.imwrite(out_filename, save_img, [cv2.IMWRITE_JPEG_QUALITY, 100])
|
|
455
809
|
add_exif_data_to_jpg_file(exif, out_filename, out_filename, verbose)
|
|
456
810
|
|
|
457
811
|
|
|
812
|
+
def exif_extra_tags_for_tif(exif):
|
|
813
|
+
res_x, res_y = exif.get(XRESOLUTION), exif.get(YRESOLUTION)
|
|
814
|
+
resolution = (
|
|
815
|
+
(res_x.numerator, res_x.denominator),
|
|
816
|
+
(res_y.numerator, res_y.denominator)
|
|
817
|
+
) if res_x and res_y else (
|
|
818
|
+
(720000, 10000), (720000, 10000)
|
|
819
|
+
)
|
|
820
|
+
exif_tags = {
|
|
821
|
+
'resolution': resolution,
|
|
822
|
+
'resolutionunit': exif.get(RESOLUTIONUNIT, 'inch'),
|
|
823
|
+
'software': clean_data_for_tiff(exif.get(SOFTWARE)) or constants.APP_TITLE,
|
|
824
|
+
'photometric': exif.get(PHOTOMETRICINTERPRETATION)
|
|
825
|
+
}
|
|
826
|
+
extra = []
|
|
827
|
+
for tag_id in exif:
|
|
828
|
+
tag, data = TAGS.get(tag_id, tag_id), exif.get(tag_id)
|
|
829
|
+
if tag in NO_COPY_TIFF_TAGS or tag_id in NO_COPY_TIFF_TAGS_ID or tag_id == SOFTWARE:
|
|
830
|
+
continue
|
|
831
|
+
if isinstance(data, IFDRational):
|
|
832
|
+
data = (data.numerator, data.denominator) if data.denominator != 0 else (0, 1)
|
|
833
|
+
extra.append((tag_id, 5, 1, data, False))
|
|
834
|
+
continue
|
|
835
|
+
processed_data = _process_tiff_data(data)
|
|
836
|
+
if processed_data:
|
|
837
|
+
dtype, count, data_value = processed_data
|
|
838
|
+
extra.append((tag_id, dtype, count, data_value, False))
|
|
839
|
+
return extra, exif_tags
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _process_tiff_data(data):
|
|
843
|
+
if isinstance(data, IFDRational):
|
|
844
|
+
data = (data.numerator, data.denominator) if data.denominator != 0 else (0, 1)
|
|
845
|
+
return 5, 1, data
|
|
846
|
+
if hasattr(data, '__iter__') and not isinstance(data, (str, bytes)):
|
|
847
|
+
try:
|
|
848
|
+
clean_data = [float(x)
|
|
849
|
+
if not hasattr(x, 'denominator') or x.denominator != 0
|
|
850
|
+
else float('nan') for x in data]
|
|
851
|
+
return 12, len(clean_data), tuple(clean_data)
|
|
852
|
+
except Exception:
|
|
853
|
+
return None
|
|
854
|
+
if isinstance(data, (str, bytes)):
|
|
855
|
+
clean_data = clean_data_for_tiff(data)
|
|
856
|
+
if clean_data:
|
|
857
|
+
return 2, len(clean_data) + 1, clean_data
|
|
858
|
+
try:
|
|
859
|
+
dtype, count = get_tiff_dtype_count(data)
|
|
860
|
+
return dtype, count, data
|
|
861
|
+
except Exception:
|
|
862
|
+
return None
|
|
863
|
+
|
|
864
|
+
|
|
458
865
|
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
866
|
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
462
|
-
|
|
463
|
-
|
|
867
|
+
try:
|
|
868
|
+
metadata = {"description": f"image generated with {constants.APP_STRING} package"}
|
|
869
|
+
extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
|
|
870
|
+
tifffile.imwrite(out_filename, image, metadata=metadata, compression='adobe_deflate',
|
|
871
|
+
extratags=extra_tags, **exif_tags)
|
|
872
|
+
except Exception:
|
|
873
|
+
tifffile.imwrite(out_filename, image, compression='adobe_deflate')
|
|
464
874
|
|
|
465
875
|
|
|
466
876
|
def write_image_with_exif_data(exif, image, out_filename, verbose=False, color_order='auto'):
|
|
@@ -485,21 +895,21 @@ def save_exif_data(exif, in_filename, out_filename=None, verbose=False):
|
|
|
485
895
|
raise RuntimeError('No exif data provided.')
|
|
486
896
|
if verbose:
|
|
487
897
|
print_exif(exif)
|
|
488
|
-
if extension_tif(in_filename):
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
898
|
+
if extension_png(in_filename) or extension_tif(in_filename):
|
|
899
|
+
if extension_tif(in_filename):
|
|
900
|
+
image_new = tifffile.imread(in_filename)
|
|
901
|
+
elif extension_png(in_filename):
|
|
902
|
+
image_new = cv2.imread(in_filename, cv2.IMREAD_UNCHANGED)
|
|
903
|
+
if extension_tif(in_filename):
|
|
904
|
+
metadata = {"description": f"image generated with {constants.APP_STRING} package"}
|
|
905
|
+
extra_tags, exif_tags = exif_extra_tags_for_tif(exif)
|
|
906
|
+
tifffile.imwrite(
|
|
907
|
+
out_filename, image_new, metadata=metadata, compression='adobe_deflate',
|
|
908
|
+
extratags=extra_tags, **exif_tags)
|
|
909
|
+
elif extension_png(in_filename):
|
|
910
|
+
write_image_with_exif_data_png(exif, image_new, out_filename, verbose)
|
|
911
|
+
else:
|
|
495
912
|
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
913
|
return exif
|
|
504
914
|
|
|
505
915
|
|
|
@@ -512,20 +922,25 @@ def copy_exif_from_file_to_file(exif_filename, in_filename, out_filename=None, v
|
|
|
512
922
|
return save_exif_data(exif, in_filename, out_filename, verbose)
|
|
513
923
|
|
|
514
924
|
|
|
515
|
-
def exif_dict(
|
|
516
|
-
if
|
|
925
|
+
def exif_dict(exif_data):
|
|
926
|
+
if exif_data is None:
|
|
517
927
|
return None
|
|
518
|
-
|
|
519
|
-
for
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
928
|
+
result = {}
|
|
929
|
+
for tag, value in exif_data.items():
|
|
930
|
+
if isinstance(tag, int):
|
|
931
|
+
tag_name = TAGS.get(tag, str(tag))
|
|
932
|
+
else:
|
|
933
|
+
tag_name = str(tag)
|
|
934
|
+
if tag_name.startswith('PNG_EXIF_'):
|
|
935
|
+
standard_tag = tag_name[9:]
|
|
936
|
+
elif tag_name.startswith('EXIF_'):
|
|
937
|
+
standard_tag = tag_name[5:]
|
|
938
|
+
elif tag_name.startswith('PNG_'):
|
|
939
|
+
continue
|
|
940
|
+
else:
|
|
941
|
+
standard_tag = tag_name
|
|
942
|
+
result[standard_tag] = (tag, value)
|
|
943
|
+
return result
|
|
529
944
|
|
|
530
945
|
|
|
531
946
|
def print_exif(exif):
|
|
@@ -538,7 +953,7 @@ def print_exif(exif):
|
|
|
538
953
|
data = f"{data.numerator}/{data.denominator}"
|
|
539
954
|
data_str = f"{data}"
|
|
540
955
|
if len(data_str) > 40:
|
|
541
|
-
data_str = f"{data_str[:40]}..."
|
|
956
|
+
data_str = f"{data_str[:40]}... (truncated)"
|
|
542
957
|
if isinstance(tag_id, int):
|
|
543
958
|
tag_id_str = f"[#{tag_id:5d}]"
|
|
544
959
|
else:
|