nt25 0.1.5__tar.gz → 0.1.7__tar.gz
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.
- {nt25-0.1.5 → nt25-0.1.7}/.gitignore +2 -2
- {nt25-0.1.5 → nt25-0.1.7}/PKG-INFO +2 -2
- {nt25-0.1.5 → nt25-0.1.7}/pyproject.toml +4 -4
- nt25-0.1.7/src/nt25/data/exif.jpg +0 -0
- nt25-0.1.7/src/nt25/lib/et.py +333 -0
- nt25-0.1.5/src/nt25/lib/et.py +0 -333
- {nt25-0.1.5 → nt25-0.1.7}/README.md +0 -0
- {nt25-0.1.5 → nt25-0.1.7}/src/nt25/__init__.py +0 -0
- {nt25-0.1.5 → nt25-0.1.7}/src/nt25/data/test.xlsx +0 -0
- {nt25-0.1.5 → nt25-0.1.7}/src/nt25/demo.py +0 -0
- {nt25-0.1.5 → nt25-0.1.7}/src/nt25/lib/calc.py +0 -0
- {nt25-0.1.5 → nt25-0.1.7}/src/nt25/lib/draw.py +0 -0
- {nt25-0.1.5 → nt25-0.1.7}/src/nt25/lib/fio.py +0 -0
- {nt25-0.1.5 → nt25-0.1.7}/tests/qr.py +0 -0
- {nt25-0.1.5 → nt25-0.1.7}/tests/test3d.py +0 -0
@@ -1,13 +1,13 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: nt25
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.7
|
4
4
|
Summary: Neo's Tools of Python
|
5
5
|
Requires-Python: >=3.10
|
6
6
|
Requires-Dist: exif>=1.6.1
|
7
7
|
Requires-Dist: matplotlib>=3.10.6
|
8
8
|
Requires-Dist: openpyxl>=3.1.5
|
9
9
|
Requires-Dist: pandas>=2.3.2
|
10
|
-
Requires-Dist:
|
10
|
+
Requires-Dist: pillow>=11.3.0
|
11
11
|
Requires-Dist: pyinstaller>=6.15.0
|
12
12
|
Requires-Dist: scikit-learn>=1.7.1
|
13
13
|
Requires-Dist: sympy>=1.14.0
|
@@ -1,18 +1,18 @@
|
|
1
1
|
[project]
|
2
2
|
name = "nt25"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.7"
|
4
4
|
description = "Neo's Tools of Python"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.10"
|
7
7
|
dependencies = [
|
8
|
+
"pyinstaller>=6.15.0",
|
8
9
|
"exif>=1.6.1",
|
9
10
|
"matplotlib>=3.10.6",
|
10
|
-
"openpyxl>=3.1.5",
|
11
11
|
"pandas>=2.3.2",
|
12
|
-
"
|
13
|
-
"pyinstaller>=6.15.0",
|
12
|
+
"openpyxl>=3.1.5",
|
14
13
|
"scikit-learn>=1.7.1",
|
15
14
|
"sympy>=1.14.0",
|
15
|
+
"pillow>=11.3.0",
|
16
16
|
]
|
17
17
|
|
18
18
|
[project.scripts]
|
Binary file
|
@@ -0,0 +1,333 @@
|
|
1
|
+
import io
|
2
|
+
import os
|
3
|
+
import time
|
4
|
+
import json
|
5
|
+
import struct
|
6
|
+
import argparse
|
7
|
+
|
8
|
+
from PIL import Image as pi
|
9
|
+
|
10
|
+
from datetime import UTC, datetime, timedelta, timezone
|
11
|
+
from exif import Image, DATETIME_STR_FORMAT
|
12
|
+
|
13
|
+
VERSION = "0.1.3"
|
14
|
+
COMMENT_SEGMENT = b"\xff\xfe"
|
15
|
+
EPOCH = datetime.fromtimestamp(0, UTC)
|
16
|
+
|
17
|
+
|
18
|
+
def dms2dec(dms: tuple):
|
19
|
+
d, m, s = dms
|
20
|
+
return d + m/60 + s/3600
|
21
|
+
|
22
|
+
|
23
|
+
def dtFormatter(str):
|
24
|
+
return datetime.strptime(str, DATETIME_STR_FORMAT)
|
25
|
+
|
26
|
+
|
27
|
+
def dt2str(dt):
|
28
|
+
return None if dt is None else dt.strftime(DATETIME_STR_FORMAT)
|
29
|
+
|
30
|
+
|
31
|
+
def gpsDt2Dt(date, time, offset=8):
|
32
|
+
d = dtFormatter(f"{date} {int(time[0])}:{int(time[1])}:{int(time[2])}")
|
33
|
+
utc = d.replace(tzinfo=timezone.utc)
|
34
|
+
return utc.astimezone(timezone(timedelta(hours=offset)))
|
35
|
+
|
36
|
+
|
37
|
+
def optimizeFile(file, q=80):
|
38
|
+
less = False
|
39
|
+
ofile = file + '.jpg'
|
40
|
+
pi.open(file).save(ofile, quality=q, optimize=True, progression=True)
|
41
|
+
|
42
|
+
if os.path.getsize(ofile) < os.path.getsize(file) * 0.9:
|
43
|
+
less = True
|
44
|
+
transplant(file, ofile)
|
45
|
+
os.replace(ofile, file)
|
46
|
+
else:
|
47
|
+
os.remove(ofile)
|
48
|
+
|
49
|
+
return less
|
50
|
+
|
51
|
+
|
52
|
+
def tryGet(img, key, default):
|
53
|
+
value = default
|
54
|
+
|
55
|
+
try:
|
56
|
+
value = img[key]
|
57
|
+
except Exception:
|
58
|
+
pass
|
59
|
+
|
60
|
+
return value
|
61
|
+
|
62
|
+
|
63
|
+
def dumpExif(file, optimize=False):
|
64
|
+
result = {}
|
65
|
+
|
66
|
+
if optimize:
|
67
|
+
optimizeFile(file)
|
68
|
+
|
69
|
+
with open(file, 'rb') as f:
|
70
|
+
img = Image(f)
|
71
|
+
for key in img.get_all():
|
72
|
+
try:
|
73
|
+
result[key] = str(img[key])
|
74
|
+
except Exception:
|
75
|
+
pass
|
76
|
+
|
77
|
+
return result
|
78
|
+
|
79
|
+
|
80
|
+
def parseExif(file, optimize=False):
|
81
|
+
if optimize:
|
82
|
+
optimizeFile(file)
|
83
|
+
|
84
|
+
with open(file, 'rb') as f:
|
85
|
+
try:
|
86
|
+
img = Image(f)
|
87
|
+
except Exception:
|
88
|
+
return {}
|
89
|
+
|
90
|
+
width = tryGet(img, 'pixel_x_dimension', -1)
|
91
|
+
height = tryGet(img, 'pixel_y_dimension', -1)
|
92
|
+
|
93
|
+
if width < 0:
|
94
|
+
width = tryGet(img, 'image_width', -1)
|
95
|
+
height = tryGet(img, 'image_height', -1)
|
96
|
+
|
97
|
+
create = tryGet(img, 'datetime_original', None)
|
98
|
+
modify = tryGet(img, 'datetime', None)
|
99
|
+
|
100
|
+
createDt = None if create is None else dtFormatter(create)
|
101
|
+
modifyDt = None if modify is None else dtFormatter(modify)
|
102
|
+
|
103
|
+
latitude = tryGet(img, 'gps_latitude', None)
|
104
|
+
latitude = None if latitude is None else dms2dec(latitude)
|
105
|
+
|
106
|
+
latRef = tryGet(img, "gps_latitude_ref", default='N')
|
107
|
+
if latRef != 'N' and latitude:
|
108
|
+
latitude = -latitude
|
109
|
+
|
110
|
+
longitude = tryGet(img, 'gps_longitude', None)
|
111
|
+
longitude = None if longitude is None else dms2dec(longitude)
|
112
|
+
|
113
|
+
longRef = tryGet(img, "gps_longitude_ref", default='E')
|
114
|
+
if longRef != 'E' and longitude:
|
115
|
+
longitude = -longitude
|
116
|
+
|
117
|
+
gpsDatetime = None
|
118
|
+
gd = tryGet(img, 'gps_datestamp', None)
|
119
|
+
gt = tryGet(img, 'gps_timestamp', None)
|
120
|
+
|
121
|
+
if gd and gt:
|
122
|
+
offset = int(time.localtime().tm_gmtoff / 3600)
|
123
|
+
gpsDatetime = gpsDt2Dt(gd, gt, offset=offset)
|
124
|
+
|
125
|
+
ts = -1 if createDt is None else int(createDt.timestamp())
|
126
|
+
mTs = -1 if modifyDt is None else int(modifyDt.timestamp())
|
127
|
+
gpsTs = -1 if gpsDatetime is None else int(gpsDatetime.timestamp())
|
128
|
+
|
129
|
+
if ts > 0:
|
130
|
+
offset = max(mTs, gpsTs) - ts
|
131
|
+
offsetDelta = str(datetime.fromtimestamp(offset, UTC) - EPOCH)
|
132
|
+
else:
|
133
|
+
offset = None
|
134
|
+
offsetDelta = None
|
135
|
+
|
136
|
+
return {
|
137
|
+
"width": width,
|
138
|
+
"height": height,
|
139
|
+
"latitude": latitude,
|
140
|
+
"longitude": longitude,
|
141
|
+
"datetime.create": dt2str(createDt),
|
142
|
+
"datetime.modify": dt2str(modifyDt),
|
143
|
+
"datetime.gps": dt2str(gpsDatetime),
|
144
|
+
"ts": ts,
|
145
|
+
"offset": offset,
|
146
|
+
"offset.delta": offsetDelta,
|
147
|
+
}
|
148
|
+
|
149
|
+
|
150
|
+
class InvalidImageDataError(ValueError):
|
151
|
+
pass
|
152
|
+
|
153
|
+
|
154
|
+
def genSegments(data):
|
155
|
+
if data[0:2] != b"\xff\xd8":
|
156
|
+
raise InvalidImageDataError("Given data isn't JPEG.")
|
157
|
+
|
158
|
+
head = 2
|
159
|
+
segments = [b"\xff\xd8"]
|
160
|
+
|
161
|
+
while 1:
|
162
|
+
if data[head: head + 2] == b"\xff\xda":
|
163
|
+
segments.append(data[head:])
|
164
|
+
break
|
165
|
+
|
166
|
+
else:
|
167
|
+
length = struct.unpack(">H", data[head + 2: head + 4])[0]
|
168
|
+
endPoint = head + length + 2
|
169
|
+
seg = data[head: endPoint]
|
170
|
+
segments.append(seg)
|
171
|
+
head = endPoint
|
172
|
+
|
173
|
+
if (head >= len(data)):
|
174
|
+
raise InvalidImageDataError("Wrong JPEG data.")
|
175
|
+
|
176
|
+
return segments
|
177
|
+
|
178
|
+
|
179
|
+
def setComment(segments, comment: str, enc='utf-8'):
|
180
|
+
contains = False
|
181
|
+
cb = comment.encode(enc)
|
182
|
+
length = len(cb) + 2
|
183
|
+
|
184
|
+
cbSeg = COMMENT_SEGMENT + length.to_bytes(2, byteorder='big') + cb
|
185
|
+
|
186
|
+
for i in range(len(segments)):
|
187
|
+
if segments[i][0:2] == COMMENT_SEGMENT:
|
188
|
+
contains = True
|
189
|
+
segments[i] = cbSeg
|
190
|
+
|
191
|
+
if not contains:
|
192
|
+
length = len(segments)
|
193
|
+
segments.insert(1 if length == 2 else length - 2, cbSeg)
|
194
|
+
|
195
|
+
return segments
|
196
|
+
|
197
|
+
|
198
|
+
def getComment(segments, enc='utf-8'):
|
199
|
+
for seg in segments:
|
200
|
+
if seg[0:2] == COMMENT_SEGMENT:
|
201
|
+
return seg[4:].decode(encoding=enc, errors='replace')
|
202
|
+
|
203
|
+
return None
|
204
|
+
|
205
|
+
|
206
|
+
def getExif(segments):
|
207
|
+
"""Returns Exif from JPEG meta data list
|
208
|
+
"""
|
209
|
+
for seg in segments:
|
210
|
+
if seg[0:2] == b"\xff\xe1" and seg[4:10] == b"Exif\x00\x00":
|
211
|
+
return seg
|
212
|
+
|
213
|
+
return b""
|
214
|
+
|
215
|
+
|
216
|
+
def mergeSegments(segments, exif=b""):
|
217
|
+
"""Merges Exif with APP0 and APP1 manipulations.
|
218
|
+
"""
|
219
|
+
if segments[1][0:2] == b"\xff\xe0" and \
|
220
|
+
segments[2][0:2] == b"\xff\xe1" and \
|
221
|
+
segments[2][4:10] == b"Exif\x00\x00":
|
222
|
+
if exif:
|
223
|
+
segments[2] = exif
|
224
|
+
segments.pop(1)
|
225
|
+
elif exif is None:
|
226
|
+
segments.pop(2)
|
227
|
+
else:
|
228
|
+
segments.pop(1)
|
229
|
+
|
230
|
+
elif segments[1][0:2] == b"\xff\xe0":
|
231
|
+
if exif:
|
232
|
+
segments[1] = exif
|
233
|
+
|
234
|
+
elif (segments[1][0:2] == b"\xff\xe1" and
|
235
|
+
segments[1][4:10] == b"Exif\x00\x00"):
|
236
|
+
|
237
|
+
if exif:
|
238
|
+
segments[1] = exif
|
239
|
+
elif exif is None:
|
240
|
+
segments.pop(1)
|
241
|
+
|
242
|
+
else:
|
243
|
+
if exif:
|
244
|
+
segments.insert(1, exif)
|
245
|
+
|
246
|
+
return b"".join(segments)
|
247
|
+
|
248
|
+
|
249
|
+
def removeExif(src, optimize=False):
|
250
|
+
if optimize:
|
251
|
+
optimizeFile(src)
|
252
|
+
|
253
|
+
with open(src, 'rb') as f:
|
254
|
+
src_data = f.read()
|
255
|
+
|
256
|
+
segments = genSegments(src_data)
|
257
|
+
segments = list(filter(lambda seg: not (seg[0:2] == b"\xff\xe1"
|
258
|
+
and seg[4:10] == b"Exif\x00\x00"),
|
259
|
+
segments))
|
260
|
+
|
261
|
+
segments = setComment(segments, "nt25.et")
|
262
|
+
new_data = b"".join(segments)
|
263
|
+
|
264
|
+
with open(src, "wb+") as f:
|
265
|
+
f.write(new_data)
|
266
|
+
|
267
|
+
|
268
|
+
def transplant(exif_src, image, optimize=False):
|
269
|
+
if optimize:
|
270
|
+
optimizeFile(image)
|
271
|
+
|
272
|
+
if exif_src[0:2] == b"\xff\xd8":
|
273
|
+
src_data = exif_src
|
274
|
+
else:
|
275
|
+
with open(exif_src, 'rb') as f:
|
276
|
+
src_data = f.read()
|
277
|
+
|
278
|
+
segments = genSegments(src_data)
|
279
|
+
exif = getExif(segments)
|
280
|
+
|
281
|
+
with open(image, 'rb') as f:
|
282
|
+
image_data = f.read()
|
283
|
+
|
284
|
+
segments = genSegments(image_data)
|
285
|
+
segments = setComment(segments, "nt25.et")
|
286
|
+
new_data = mergeSegments(segments, exif)
|
287
|
+
|
288
|
+
with open(image, "wb+") as f:
|
289
|
+
f.write(new_data)
|
290
|
+
|
291
|
+
|
292
|
+
def main():
|
293
|
+
parser = argparse.ArgumentParser(description="EXIF tool")
|
294
|
+
parser.add_argument('-v', '--version', action='store_true',
|
295
|
+
help='echo version')
|
296
|
+
parser.add_argument('-o', '--optimize', action='store_true',
|
297
|
+
help='optimize jpg file, work with -r, -d, -c, -f')
|
298
|
+
parser.add_argument('-r', '--rm', action='store_true',
|
299
|
+
help='remove meta, use: -r -f FILE')
|
300
|
+
parser.add_argument('-d', '--dump', action='store_true',
|
301
|
+
help='dump meta, use: -d -f FILE')
|
302
|
+
parser.add_argument('-c', '--copy', type=str,
|
303
|
+
help='copy meta, use: -c SRC -f DST')
|
304
|
+
parser.add_argument('-f', '--file', type=str, help='image file')
|
305
|
+
|
306
|
+
args = parser.parse_args()
|
307
|
+
|
308
|
+
if args.version:
|
309
|
+
print(f"et: {VERSION}")
|
310
|
+
return
|
311
|
+
|
312
|
+
if args.file is None:
|
313
|
+
print("usage: et [-h] [-v] [-o] [-f FILE] [-d -f FILE]\n"
|
314
|
+
"\t\t[-r -f FILE] [-c SRC -f DST]")
|
315
|
+
return
|
316
|
+
|
317
|
+
opt = True if args.optimize else False
|
318
|
+
|
319
|
+
if args.dump:
|
320
|
+
r = dumpExif(args.file, optimize=opt)
|
321
|
+
elif args.rm:
|
322
|
+
r = removeExif(args.file, optimize=opt)
|
323
|
+
elif args.copy:
|
324
|
+
r = transplant(args.copy, args.file, optimize=opt)
|
325
|
+
else:
|
326
|
+
r = parseExif(args.file, optimize=opt)
|
327
|
+
|
328
|
+
if r is not None:
|
329
|
+
print(json.dumps(r, indent=2, sort_keys=False))
|
330
|
+
|
331
|
+
|
332
|
+
if __name__ == "__main__":
|
333
|
+
main()
|
nt25-0.1.5/src/nt25/lib/et.py
DELETED
@@ -1,333 +0,0 @@
|
|
1
|
-
import io
|
2
|
-
import time
|
3
|
-
import json
|
4
|
-
import struct
|
5
|
-
import argparse
|
6
|
-
|
7
|
-
from datetime import UTC, datetime, timedelta, timezone
|
8
|
-
from exif import Image, DATETIME_STR_FORMAT
|
9
|
-
|
10
|
-
VERSION = "0.1.2"
|
11
|
-
EPOCH = datetime.fromtimestamp(0, UTC)
|
12
|
-
|
13
|
-
|
14
|
-
def dms2dec(dms: tuple):
|
15
|
-
d, m, s = dms
|
16
|
-
return d + m/60 + s/3600
|
17
|
-
|
18
|
-
|
19
|
-
def dtFormatter(str):
|
20
|
-
return datetime.strptime(str, DATETIME_STR_FORMAT)
|
21
|
-
|
22
|
-
|
23
|
-
def dt2str(dt):
|
24
|
-
return None if dt is None else dt.strftime(DATETIME_STR_FORMAT)
|
25
|
-
|
26
|
-
|
27
|
-
def gpsDt2Dt(date, time, offset=8):
|
28
|
-
d = dtFormatter(f"{date} {int(time[0])}:{int(time[1])}:{int(time[2])}")
|
29
|
-
utc = d.replace(tzinfo=timezone.utc)
|
30
|
-
return utc.astimezone(timezone(timedelta(hours=offset)))
|
31
|
-
|
32
|
-
|
33
|
-
def tryGet(img, key, default):
|
34
|
-
value = default
|
35
|
-
|
36
|
-
try:
|
37
|
-
value = img[key]
|
38
|
-
except Exception:
|
39
|
-
pass
|
40
|
-
|
41
|
-
return value
|
42
|
-
|
43
|
-
|
44
|
-
def dumpExif(file):
|
45
|
-
result = {}
|
46
|
-
with open(file, 'rb') as f:
|
47
|
-
img = Image(f)
|
48
|
-
for key in img.get_all():
|
49
|
-
try:
|
50
|
-
result[key] = str(img[key])
|
51
|
-
except Exception:
|
52
|
-
pass
|
53
|
-
|
54
|
-
return result
|
55
|
-
|
56
|
-
|
57
|
-
def parseExif(file):
|
58
|
-
with open(file, 'rb') as f:
|
59
|
-
try:
|
60
|
-
img = Image(f)
|
61
|
-
except Exception:
|
62
|
-
return {}
|
63
|
-
|
64
|
-
width = tryGet(img, 'pixel_x_dimension', -1)
|
65
|
-
height = tryGet(img, 'pixel_y_dimension', -1)
|
66
|
-
|
67
|
-
if width < 0:
|
68
|
-
width = tryGet(img, 'image_width', -1)
|
69
|
-
height = tryGet(img, 'image_height', -1)
|
70
|
-
|
71
|
-
create = tryGet(img, 'datetime_original', None)
|
72
|
-
modify = tryGet(img, 'datetime', None)
|
73
|
-
|
74
|
-
createDt = None if create is None else dtFormatter(create)
|
75
|
-
modifyDt = None if modify is None else dtFormatter(modify)
|
76
|
-
|
77
|
-
latitude = tryGet(img, 'gps_latitude', None)
|
78
|
-
latitude = None if latitude is None else dms2dec(latitude)
|
79
|
-
|
80
|
-
longitude = tryGet(img, 'gps_longitude', None)
|
81
|
-
longitude = None if longitude is None else dms2dec(longitude)
|
82
|
-
|
83
|
-
gpsDatetime = None
|
84
|
-
gd = tryGet(img, 'gps_datestamp', None)
|
85
|
-
gt = tryGet(img, 'gps_timestamp', None)
|
86
|
-
|
87
|
-
if gd and gt:
|
88
|
-
offset = int(time.localtime().tm_gmtoff / 3600)
|
89
|
-
gpsDatetime = gpsDt2Dt(gd, gt, offset=offset)
|
90
|
-
|
91
|
-
ts = -1 if createDt is None else int(createDt.timestamp())
|
92
|
-
mTs = -1 if modifyDt is None else int(modifyDt.timestamp())
|
93
|
-
gpsTs = -1 if gpsDatetime is None else int(gpsDatetime.timestamp())
|
94
|
-
offset = max(mTs, gpsTs) - ts
|
95
|
-
offsetDelta = datetime.fromtimestamp(offset, UTC) - EPOCH
|
96
|
-
|
97
|
-
return {
|
98
|
-
"width": width,
|
99
|
-
"height": height,
|
100
|
-
"latitude": latitude,
|
101
|
-
"longitude": longitude,
|
102
|
-
"datetime.create": dt2str(createDt),
|
103
|
-
"datetime.modify": dt2str(modifyDt),
|
104
|
-
"datetime.gps": dt2str(gpsDatetime),
|
105
|
-
"ts": ts,
|
106
|
-
"offset": offset,
|
107
|
-
"offset.delta": str(offsetDelta),
|
108
|
-
}
|
109
|
-
|
110
|
-
|
111
|
-
class InvalidImageDataError(ValueError):
|
112
|
-
pass
|
113
|
-
|
114
|
-
|
115
|
-
def split_into_segments(data):
|
116
|
-
"""Slices JPEG meta data into a list from JPEG binary data.
|
117
|
-
"""
|
118
|
-
if data[0:2] != b"\xff\xd8":
|
119
|
-
raise InvalidImageDataError("Given data isn't JPEG.")
|
120
|
-
|
121
|
-
head = 2
|
122
|
-
segments = [b"\xff\xd8"]
|
123
|
-
while 1:
|
124
|
-
if data[head: head + 2] == b"\xff\xda":
|
125
|
-
segments.append(data[head:])
|
126
|
-
break
|
127
|
-
else:
|
128
|
-
length = struct.unpack(">H", data[head + 2: head + 4])[0]
|
129
|
-
endPoint = head + length + 2
|
130
|
-
seg = data[head: endPoint]
|
131
|
-
segments.append(seg)
|
132
|
-
head = endPoint
|
133
|
-
|
134
|
-
if (head >= len(data)):
|
135
|
-
raise InvalidImageDataError("Wrong JPEG data.")
|
136
|
-
|
137
|
-
return segments
|
138
|
-
|
139
|
-
|
140
|
-
def read_exif_from_file(filename):
|
141
|
-
"""Slices JPEG meta data into a list from JPEG binary data.
|
142
|
-
"""
|
143
|
-
f = open(filename, "rb")
|
144
|
-
data = f.read(6)
|
145
|
-
|
146
|
-
if data[0:2] != b"\xff\xd8":
|
147
|
-
raise InvalidImageDataError("Given data isn't JPEG.")
|
148
|
-
|
149
|
-
head = data[2:6]
|
150
|
-
HEAD_LENGTH = 4
|
151
|
-
exif = None
|
152
|
-
while len(head) == HEAD_LENGTH:
|
153
|
-
length = struct.unpack(">H", head[2: 4])[0]
|
154
|
-
|
155
|
-
if head[:2] == b"\xff\xe1":
|
156
|
-
segment_data = f.read(length - 2)
|
157
|
-
if segment_data[:4] != b'Exif':
|
158
|
-
head = f.read(HEAD_LENGTH)
|
159
|
-
continue
|
160
|
-
exif = head + segment_data
|
161
|
-
break
|
162
|
-
elif head[0:1] == b"\xff":
|
163
|
-
f.read(length - 2)
|
164
|
-
head = f.read(HEAD_LENGTH)
|
165
|
-
else:
|
166
|
-
break
|
167
|
-
|
168
|
-
f.close()
|
169
|
-
return exif
|
170
|
-
|
171
|
-
|
172
|
-
def get_exif_seg(segments):
|
173
|
-
"""Returns Exif from JPEG meta data list
|
174
|
-
"""
|
175
|
-
for seg in segments:
|
176
|
-
if seg[0:2] == b"\xff\xe1" and seg[4:10] == b"Exif\x00\x00":
|
177
|
-
return seg
|
178
|
-
|
179
|
-
return None
|
180
|
-
|
181
|
-
|
182
|
-
def merge_segments(segments, exif=b""):
|
183
|
-
"""Merges Exif with APP0 and APP1 manipulations.
|
184
|
-
"""
|
185
|
-
if segments[1][0:2] == b"\xff\xe0" and \
|
186
|
-
segments[2][0:2] == b"\xff\xe1" and \
|
187
|
-
segments[2][4:10] == b"Exif\x00\x00":
|
188
|
-
if exif:
|
189
|
-
segments[2] = exif
|
190
|
-
segments.pop(1)
|
191
|
-
elif exif is None:
|
192
|
-
segments.pop(2)
|
193
|
-
else:
|
194
|
-
segments.pop(1)
|
195
|
-
|
196
|
-
elif segments[1][0:2] == b"\xff\xe0":
|
197
|
-
if exif:
|
198
|
-
segments[1] = exif
|
199
|
-
|
200
|
-
elif (segments[1][0:2] == b"\xff\xe1" and
|
201
|
-
segments[1][4:10] == b"Exif\x00\x00"):
|
202
|
-
|
203
|
-
if exif:
|
204
|
-
segments[1] = exif
|
205
|
-
elif exif is None:
|
206
|
-
segments.pop(1)
|
207
|
-
|
208
|
-
else:
|
209
|
-
if exif:
|
210
|
-
segments.insert(1, exif)
|
211
|
-
|
212
|
-
return b"".join(segments)
|
213
|
-
|
214
|
-
|
215
|
-
def remove(src, new_file=None):
|
216
|
-
"""
|
217
|
-
py:function:: piexif.remove(filename)
|
218
|
-
|
219
|
-
Remove exif from JPEG.
|
220
|
-
|
221
|
-
:param str filename: JPEG
|
222
|
-
"""
|
223
|
-
output_is_file = False
|
224
|
-
if src[0:2] == b"\xff\xd8":
|
225
|
-
src_data = src
|
226
|
-
file_type = "jpeg"
|
227
|
-
else:
|
228
|
-
with open(src, 'rb') as f:
|
229
|
-
src_data = f.read()
|
230
|
-
output_is_file = True
|
231
|
-
if src_data[0:2] == b"\xff\xd8":
|
232
|
-
file_type = "jpeg"
|
233
|
-
|
234
|
-
if file_type == "jpeg":
|
235
|
-
segments = split_into_segments(src_data)
|
236
|
-
exif = get_exif_seg(segments)
|
237
|
-
if exif:
|
238
|
-
new_data = src_data.replace(exif, b"")
|
239
|
-
else:
|
240
|
-
new_data = src_data
|
241
|
-
|
242
|
-
if isinstance(new_file, io.BytesIO):
|
243
|
-
new_file.write(new_data)
|
244
|
-
new_file.seek(0)
|
245
|
-
elif new_file:
|
246
|
-
with open(new_file, "wb+") as f:
|
247
|
-
f.write(new_data)
|
248
|
-
elif output_is_file:
|
249
|
-
with open(src, "wb+") as f:
|
250
|
-
f.write(new_data)
|
251
|
-
else:
|
252
|
-
raise ValueError("Give a second argument to 'remove' to output file")
|
253
|
-
|
254
|
-
|
255
|
-
def transplant(exif_src, image, new_file=None):
|
256
|
-
"""
|
257
|
-
py:function:: piexif.transplant(filename1, filename2)
|
258
|
-
|
259
|
-
Transplant exif from filename1 to filename2.
|
260
|
-
|
261
|
-
:param str filename1: JPEG
|
262
|
-
:param str filename2: JPEG
|
263
|
-
"""
|
264
|
-
if exif_src[0:2] == b"\xff\xd8":
|
265
|
-
src_data = exif_src
|
266
|
-
else:
|
267
|
-
with open(exif_src, 'rb') as f:
|
268
|
-
src_data = f.read()
|
269
|
-
|
270
|
-
segments = split_into_segments(src_data)
|
271
|
-
exif = get_exif_seg(segments)
|
272
|
-
|
273
|
-
if exif is None:
|
274
|
-
raise ValueError("not found exif in input")
|
275
|
-
|
276
|
-
output_file = False
|
277
|
-
if image[0:2] == b"\xff\xd8":
|
278
|
-
image_data = image
|
279
|
-
else:
|
280
|
-
with open(image, 'rb') as f:
|
281
|
-
image_data = f.read()
|
282
|
-
output_file = True
|
283
|
-
|
284
|
-
segments = split_into_segments(image_data)
|
285
|
-
new_data = merge_segments(segments, exif)
|
286
|
-
|
287
|
-
if isinstance(new_file, io.BytesIO):
|
288
|
-
new_file.write(new_data)
|
289
|
-
new_file.seek(0)
|
290
|
-
elif new_file:
|
291
|
-
with open(new_file, "wb+") as f:
|
292
|
-
f.write(new_data)
|
293
|
-
elif output_file:
|
294
|
-
with open(image, "wb+") as f:
|
295
|
-
f.write(new_data)
|
296
|
-
else:
|
297
|
-
raise ValueError("Give a 3rd argument to 'transplant' to output file")
|
298
|
-
|
299
|
-
|
300
|
-
def main():
|
301
|
-
parser = argparse.ArgumentParser(description="EXIF tool")
|
302
|
-
parser.add_argument('-v', '--version',
|
303
|
-
help='echo version', action='store_true')
|
304
|
-
parser.add_argument('-d', '--dump', help='dump meta', action='store_true')
|
305
|
-
parser.add_argument('-r', '--rm', help='remove meta', action='store_true')
|
306
|
-
parser.add_argument('-c', '--copy', type=str, help='copy meta')
|
307
|
-
parser.add_argument('-f', '--file', type=str, help='image file')
|
308
|
-
|
309
|
-
args = parser.parse_args()
|
310
|
-
|
311
|
-
if args.version:
|
312
|
-
print(f"et: {VERSION}")
|
313
|
-
return
|
314
|
-
|
315
|
-
if args.file is None:
|
316
|
-
print("usage: et [-h] [-v] [-r FILE] [-c FILE] [-d] [-f FILE]")
|
317
|
-
return
|
318
|
-
|
319
|
-
if args.dump:
|
320
|
-
r = dumpExif(args.file)
|
321
|
-
elif args.rm:
|
322
|
-
r = remove(args.file)
|
323
|
-
elif args.copy:
|
324
|
-
r = transplant(args.copy, args.file)
|
325
|
-
else:
|
326
|
-
r = parseExif(args.file)
|
327
|
-
|
328
|
-
if r is not None:
|
329
|
-
print(json.dumps(r, indent=2, sort_keys=False))
|
330
|
-
|
331
|
-
|
332
|
-
if __name__ == "__main__":
|
333
|
-
main()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|