nt25 0.1.5__py3-none-any.whl → 0.1.7__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.
nt25/data/exif.jpg
ADDED
Binary file
|
nt25/lib/et.py
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
import io
|
2
|
+
import os
|
2
3
|
import time
|
3
4
|
import json
|
4
5
|
import struct
|
5
6
|
import argparse
|
6
7
|
|
8
|
+
from PIL import Image as pi
|
9
|
+
|
7
10
|
from datetime import UTC, datetime, timedelta, timezone
|
8
11
|
from exif import Image, DATETIME_STR_FORMAT
|
9
12
|
|
10
|
-
VERSION = "0.1.
|
13
|
+
VERSION = "0.1.3"
|
14
|
+
COMMENT_SEGMENT = b"\xff\xfe"
|
11
15
|
EPOCH = datetime.fromtimestamp(0, UTC)
|
12
16
|
|
13
17
|
|
@@ -30,6 +34,21 @@ def gpsDt2Dt(date, time, offset=8):
|
|
30
34
|
return utc.astimezone(timezone(timedelta(hours=offset)))
|
31
35
|
|
32
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
|
+
|
33
52
|
def tryGet(img, key, default):
|
34
53
|
value = default
|
35
54
|
|
@@ -41,8 +60,12 @@ def tryGet(img, key, default):
|
|
41
60
|
return value
|
42
61
|
|
43
62
|
|
44
|
-
def dumpExif(file):
|
63
|
+
def dumpExif(file, optimize=False):
|
45
64
|
result = {}
|
65
|
+
|
66
|
+
if optimize:
|
67
|
+
optimizeFile(file)
|
68
|
+
|
46
69
|
with open(file, 'rb') as f:
|
47
70
|
img = Image(f)
|
48
71
|
for key in img.get_all():
|
@@ -54,76 +77,92 @@ def dumpExif(file):
|
|
54
77
|
return result
|
55
78
|
|
56
79
|
|
57
|
-
def parseExif(file):
|
80
|
+
def parseExif(file, optimize=False):
|
81
|
+
if optimize:
|
82
|
+
optimizeFile(file)
|
83
|
+
|
58
84
|
with open(file, 'rb') as f:
|
59
85
|
try:
|
60
86
|
img = Image(f)
|
61
87
|
except Exception:
|
62
88
|
return {}
|
63
89
|
|
64
|
-
|
65
|
-
|
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)
|
66
99
|
|
67
|
-
|
68
|
-
|
69
|
-
height = tryGet(img, 'image_height', -1)
|
100
|
+
createDt = None if create is None else dtFormatter(create)
|
101
|
+
modifyDt = None if modify is None else dtFormatter(modify)
|
70
102
|
|
71
|
-
|
72
|
-
|
103
|
+
latitude = tryGet(img, 'gps_latitude', None)
|
104
|
+
latitude = None if latitude is None else dms2dec(latitude)
|
73
105
|
|
74
|
-
|
75
|
-
|
106
|
+
latRef = tryGet(img, "gps_latitude_ref", default='N')
|
107
|
+
if latRef != 'N' and latitude:
|
108
|
+
latitude = -latitude
|
76
109
|
|
77
|
-
|
78
|
-
|
110
|
+
longitude = tryGet(img, 'gps_longitude', None)
|
111
|
+
longitude = None if longitude is None else dms2dec(longitude)
|
79
112
|
|
80
|
-
|
81
|
-
|
113
|
+
longRef = tryGet(img, "gps_longitude_ref", default='E')
|
114
|
+
if longRef != 'E' and longitude:
|
115
|
+
longitude = -longitude
|
82
116
|
|
83
|
-
|
84
|
-
|
85
|
-
|
117
|
+
gpsDatetime = None
|
118
|
+
gd = tryGet(img, 'gps_datestamp', None)
|
119
|
+
gt = tryGet(img, 'gps_timestamp', None)
|
86
120
|
|
87
|
-
|
88
|
-
|
89
|
-
|
121
|
+
if gd and gt:
|
122
|
+
offset = int(time.localtime().tm_gmtoff / 3600)
|
123
|
+
gpsDatetime = gpsDt2Dt(gd, gt, offset=offset)
|
90
124
|
|
91
|
-
|
92
|
-
|
93
|
-
|
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:
|
94
130
|
offset = max(mTs, gpsTs) - ts
|
95
|
-
offsetDelta = datetime.fromtimestamp(offset, UTC) - EPOCH
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
+
}
|
109
148
|
|
110
149
|
|
111
150
|
class InvalidImageDataError(ValueError):
|
112
151
|
pass
|
113
152
|
|
114
153
|
|
115
|
-
def
|
116
|
-
"""Slices JPEG meta data into a list from JPEG binary data.
|
117
|
-
"""
|
154
|
+
def genSegments(data):
|
118
155
|
if data[0:2] != b"\xff\xd8":
|
119
156
|
raise InvalidImageDataError("Given data isn't JPEG.")
|
120
157
|
|
121
158
|
head = 2
|
122
159
|
segments = [b"\xff\xd8"]
|
160
|
+
|
123
161
|
while 1:
|
124
162
|
if data[head: head + 2] == b"\xff\xda":
|
125
163
|
segments.append(data[head:])
|
126
164
|
break
|
165
|
+
|
127
166
|
else:
|
128
167
|
length = struct.unpack(">H", data[head + 2: head + 4])[0]
|
129
168
|
endPoint = head + length + 2
|
@@ -137,49 +176,44 @@ def split_into_segments(data):
|
|
137
176
|
return segments
|
138
177
|
|
139
178
|
|
140
|
-
def
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
data = f.read(6)
|
179
|
+
def setComment(segments, comment: str, enc='utf-8'):
|
180
|
+
contains = False
|
181
|
+
cb = comment.encode(enc)
|
182
|
+
length = len(cb) + 2
|
145
183
|
|
146
|
-
|
147
|
-
raise InvalidImageDataError("Given data isn't JPEG.")
|
184
|
+
cbSeg = COMMENT_SEGMENT + length.to_bytes(2, byteorder='big') + cb
|
148
185
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
186
|
+
for i in range(len(segments)):
|
187
|
+
if segments[i][0:2] == COMMENT_SEGMENT:
|
188
|
+
contains = True
|
189
|
+
segments[i] = cbSeg
|
167
190
|
|
168
|
-
|
169
|
-
|
191
|
+
if not contains:
|
192
|
+
length = len(segments)
|
193
|
+
segments.insert(1 if length == 2 else length - 2, cbSeg)
|
170
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
|
171
204
|
|
172
|
-
|
205
|
+
|
206
|
+
def getExif(segments):
|
173
207
|
"""Returns Exif from JPEG meta data list
|
174
208
|
"""
|
175
209
|
for seg in segments:
|
176
210
|
if seg[0:2] == b"\xff\xe1" and seg[4:10] == b"Exif\x00\x00":
|
177
211
|
return seg
|
178
212
|
|
179
|
-
return
|
213
|
+
return b""
|
180
214
|
|
181
215
|
|
182
|
-
def
|
216
|
+
def mergeSegments(segments, exif=b""):
|
183
217
|
"""Merges Exif with APP0 and APP1 manipulations.
|
184
218
|
"""
|
185
219
|
if segments[1][0:2] == b"\xff\xe0" and \
|
@@ -212,98 +246,61 @@ def merge_segments(segments, exif=b""):
|
|
212
246
|
return b"".join(segments)
|
213
247
|
|
214
248
|
|
215
|
-
def
|
216
|
-
|
217
|
-
|
249
|
+
def removeExif(src, optimize=False):
|
250
|
+
if optimize:
|
251
|
+
optimizeFile(src)
|
218
252
|
|
219
|
-
|
253
|
+
with open(src, 'rb') as f:
|
254
|
+
src_data = f.read()
|
220
255
|
|
221
|
-
|
222
|
-
""
|
223
|
-
|
224
|
-
|
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"
|
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))
|
233
260
|
|
234
|
-
|
235
|
-
|
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")
|
261
|
+
segments = setComment(segments, "nt25.et")
|
262
|
+
new_data = b"".join(segments)
|
253
263
|
|
264
|
+
with open(src, "wb+") as f:
|
265
|
+
f.write(new_data)
|
254
266
|
|
255
|
-
def transplant(exif_src, image, new_file=None):
|
256
|
-
"""
|
257
|
-
py:function:: piexif.transplant(filename1, filename2)
|
258
267
|
|
259
|
-
|
268
|
+
def transplant(exif_src, image, optimize=False):
|
269
|
+
if optimize:
|
270
|
+
optimizeFile(image)
|
260
271
|
|
261
|
-
:param str filename1: JPEG
|
262
|
-
:param str filename2: JPEG
|
263
|
-
"""
|
264
272
|
if exif_src[0:2] == b"\xff\xd8":
|
265
273
|
src_data = exif_src
|
266
274
|
else:
|
267
275
|
with open(exif_src, 'rb') as f:
|
268
276
|
src_data = f.read()
|
269
277
|
|
270
|
-
segments =
|
271
|
-
exif =
|
278
|
+
segments = genSegments(src_data)
|
279
|
+
exif = getExif(segments)
|
272
280
|
|
273
|
-
|
274
|
-
|
281
|
+
with open(image, 'rb') as f:
|
282
|
+
image_data = f.read()
|
275
283
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
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")
|
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)
|
298
290
|
|
299
291
|
|
300
292
|
def main():
|
301
293
|
parser = argparse.ArgumentParser(description="EXIF tool")
|
302
|
-
parser.add_argument('-v', '--version',
|
303
|
-
help='echo version'
|
304
|
-
parser.add_argument('-
|
305
|
-
|
306
|
-
parser.add_argument('-
|
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')
|
307
304
|
parser.add_argument('-f', '--file', type=str, help='image file')
|
308
305
|
|
309
306
|
args = parser.parse_args()
|
@@ -313,17 +310,20 @@ def main():
|
|
313
310
|
return
|
314
311
|
|
315
312
|
if args.file is None:
|
316
|
-
print("usage: et [-h] [-v] [-
|
313
|
+
print("usage: et [-h] [-v] [-o] [-f FILE] [-d -f FILE]\n"
|
314
|
+
"\t\t[-r -f FILE] [-c SRC -f DST]")
|
317
315
|
return
|
318
316
|
|
317
|
+
opt = True if args.optimize else False
|
318
|
+
|
319
319
|
if args.dump:
|
320
|
-
r = dumpExif(args.file)
|
320
|
+
r = dumpExif(args.file, optimize=opt)
|
321
321
|
elif args.rm:
|
322
|
-
r =
|
322
|
+
r = removeExif(args.file, optimize=opt)
|
323
323
|
elif args.copy:
|
324
|
-
r = transplant(args.copy, args.file)
|
324
|
+
r = transplant(args.copy, args.file, optimize=opt)
|
325
325
|
else:
|
326
|
-
r = parseExif(args.file)
|
326
|
+
r = parseExif(args.file, optimize=opt)
|
327
327
|
|
328
328
|
if r is not None:
|
329
329
|
print(json.dumps(r, indent=2, sort_keys=False))
|
@@ -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,11 +1,12 @@
|
|
1
1
|
nt25/__init__.py,sha256=28wWlyuyScDrZx9ytGM_TxUipk5dg4pOyp_xyj6-NWk,295
|
2
2
|
nt25/demo.py,sha256=W34-7KTYVpBYXg0UrgmkVCRWAdO5RAc1K7zS9pz-BEQ,2179
|
3
|
+
nt25/data/exif.jpg,sha256=Wl9QqZ3UAC_ldd055lJOcZKqvU5swqLEw_HxRBbLYgc,347767
|
3
4
|
nt25/data/test.xlsx,sha256=7C0JDS-TLm_KmjnKtfeajkpwGKSUhcLdr2W2UFUxAgM,10542
|
4
5
|
nt25/lib/calc.py,sha256=3X3k9jisSjRP7OokSdKvoVo4IIOzk2efexW8z1gMo-w,2265
|
5
6
|
nt25/lib/draw.py,sha256=OKTlkkNVUz_LGBA9Gk7fjcnbbbl7e_hT8nWKkcfeg2k,5642
|
6
|
-
nt25/lib/et.py,sha256=
|
7
|
+
nt25/lib/et.py,sha256=uhTwNTM3NRElxvw6jiMHjmg6vrS5K2Fj3mj-YjQs_iM,7873
|
7
8
|
nt25/lib/fio.py,sha256=WvHpG6QYR1NE19Ss3Sy2FdajTxibX5SVW3PyC5Y5Krk,2525
|
8
|
-
nt25-0.1.
|
9
|
-
nt25-0.1.
|
10
|
-
nt25-0.1.
|
11
|
-
nt25-0.1.
|
9
|
+
nt25-0.1.7.dist-info/METADATA,sha256=oohtP4yldEAS5G9w2tEFkntYpBKzYqMloax9Ui6wf4k,684
|
10
|
+
nt25-0.1.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
11
|
+
nt25-0.1.7.dist-info/entry_points.txt,sha256=mtt7YI92CecNLeMvyM3o5IyVzRpYSqqwmUgAzldhFH8,62
|
12
|
+
nt25-0.1.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|