nt25 0.1.6__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.6 → nt25-0.1.7}/PKG-INFO +2 -1
- {nt25-0.1.6 → nt25-0.1.7}/pyproject.toml +2 -1
- {nt25-0.1.6 → nt25-0.1.7}/src/nt25/lib/et.py +118 -111
- {nt25-0.1.6 → nt25-0.1.7}/.gitignore +0 -0
- {nt25-0.1.6 → nt25-0.1.7}/README.md +0 -0
- {nt25-0.1.6 → nt25-0.1.7}/src/nt25/__init__.py +0 -0
- {nt25-0.1.6 → nt25-0.1.7}/src/nt25/data/exif.jpg +0 -0
- {nt25-0.1.6 → nt25-0.1.7}/src/nt25/data/test.xlsx +0 -0
- {nt25-0.1.6 → nt25-0.1.7}/src/nt25/demo.py +0 -0
- {nt25-0.1.6 → nt25-0.1.7}/src/nt25/lib/calc.py +0 -0
- {nt25-0.1.6 → nt25-0.1.7}/src/nt25/lib/draw.py +0 -0
- {nt25-0.1.6 → nt25-0.1.7}/src/nt25/lib/fio.py +0 -0
- {nt25-0.1.6 → nt25-0.1.7}/tests/qr.py +0 -0
- {nt25-0.1.6 → nt25-0.1.7}/tests/test3d.py +0 -0
@@ -1,12 +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: pillow>=11.3.0
|
10
11
|
Requires-Dist: pyinstaller>=6.15.0
|
11
12
|
Requires-Dist: scikit-learn>=1.7.1
|
12
13
|
Requires-Dist: sympy>=1.14.0
|
@@ -1,6 +1,6 @@
|
|
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"
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
12
12
|
"openpyxl>=3.1.5",
|
13
13
|
"scikit-learn>=1.7.1",
|
14
14
|
"sympy>=1.14.0",
|
15
|
+
"pillow>=11.3.0",
|
15
16
|
]
|
16
17
|
|
17
18
|
[project.scripts]
|
@@ -1,14 +1,16 @@
|
|
1
1
|
import io
|
2
|
-
|
2
|
+
import os
|
3
3
|
import time
|
4
4
|
import json
|
5
5
|
import struct
|
6
6
|
import argparse
|
7
7
|
|
8
|
+
from PIL import Image as pi
|
9
|
+
|
8
10
|
from datetime import UTC, datetime, timedelta, timezone
|
9
11
|
from exif import Image, DATETIME_STR_FORMAT
|
10
12
|
|
11
|
-
VERSION = "0.1.
|
13
|
+
VERSION = "0.1.3"
|
12
14
|
COMMENT_SEGMENT = b"\xff\xfe"
|
13
15
|
EPOCH = datetime.fromtimestamp(0, UTC)
|
14
16
|
|
@@ -32,6 +34,21 @@ def gpsDt2Dt(date, time, offset=8):
|
|
32
34
|
return utc.astimezone(timezone(timedelta(hours=offset)))
|
33
35
|
|
34
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
|
+
|
35
52
|
def tryGet(img, key, default):
|
36
53
|
value = default
|
37
54
|
|
@@ -43,9 +60,12 @@ def tryGet(img, key, default):
|
|
43
60
|
return value
|
44
61
|
|
45
62
|
|
46
|
-
def dumpExif(file):
|
63
|
+
def dumpExif(file, optimize=False):
|
47
64
|
result = {}
|
48
65
|
|
66
|
+
if optimize:
|
67
|
+
optimizeFile(file)
|
68
|
+
|
49
69
|
with open(file, 'rb') as f:
|
50
70
|
img = Image(f)
|
51
71
|
for key in img.get_all():
|
@@ -57,58 +77,74 @@ def dumpExif(file):
|
|
57
77
|
return result
|
58
78
|
|
59
79
|
|
60
|
-
def parseExif(file):
|
80
|
+
def parseExif(file, optimize=False):
|
81
|
+
if optimize:
|
82
|
+
optimizeFile(file)
|
83
|
+
|
61
84
|
with open(file, 'rb') as f:
|
62
85
|
try:
|
63
86
|
img = Image(f)
|
64
87
|
except Exception:
|
65
88
|
return {}
|
66
89
|
|
67
|
-
|
68
|
-
|
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)
|
69
102
|
|
70
|
-
|
71
|
-
|
72
|
-
height = tryGet(img, 'image_height', -1)
|
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
|
-
|
117
|
+
gpsDatetime = None
|
118
|
+
gd = tryGet(img, 'gps_datestamp', None)
|
119
|
+
gt = tryGet(img, 'gps_timestamp', None)
|
85
120
|
|
86
|
-
|
87
|
-
|
88
|
-
|
121
|
+
if gd and gt:
|
122
|
+
offset = int(time.localtime().tm_gmtoff / 3600)
|
123
|
+
gpsDatetime = gpsDt2Dt(gd, gt, offset=offset)
|
89
124
|
|
90
|
-
|
91
|
-
|
92
|
-
|
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())
|
93
128
|
|
94
|
-
|
95
|
-
mTs = -1 if modifyDt is None else int(modifyDt.timestamp())
|
96
|
-
gpsTs = -1 if gpsDatetime is None else int(gpsDatetime.timestamp())
|
129
|
+
if ts > 0:
|
97
130
|
offset = max(mTs, gpsTs) - ts
|
98
|
-
offsetDelta = datetime.fromtimestamp(offset, UTC) - EPOCH
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
+
}
|
112
148
|
|
113
149
|
|
114
150
|
class InvalidImageDataError(ValueError):
|
@@ -174,7 +210,7 @@ def getExif(segments):
|
|
174
210
|
if seg[0:2] == b"\xff\xe1" and seg[4:10] == b"Exif\x00\x00":
|
175
211
|
return seg
|
176
212
|
|
177
|
-
return
|
213
|
+
return b""
|
178
214
|
|
179
215
|
|
180
216
|
def mergeSegments(segments, exif=b""):
|
@@ -210,49 +246,29 @@ def mergeSegments(segments, exif=b""):
|
|
210
246
|
return b"".join(segments)
|
211
247
|
|
212
248
|
|
213
|
-
def removeExif(src,
|
214
|
-
|
215
|
-
|
216
|
-
src_data = src
|
217
|
-
file_type = "jpeg"
|
218
|
-
else:
|
219
|
-
with open(src, 'rb') as f:
|
220
|
-
src_data = f.read()
|
221
|
-
output_is_file = True
|
222
|
-
if src_data[0:2] == b"\xff\xd8":
|
223
|
-
file_type = "jpeg"
|
224
|
-
|
225
|
-
if file_type == "jpeg":
|
226
|
-
segments = genSegments(src_data)
|
227
|
-
segments = list(filter(lambda seg: not (seg[0:2] == b"\xff\xe1"
|
228
|
-
and seg[4:10] == b"Exif\x00\x00"),
|
229
|
-
segments))
|
230
|
-
|
231
|
-
segments = setComment(segments, "nt25.et")
|
232
|
-
new_data = b"".join(segments)
|
233
|
-
|
234
|
-
if isinstance(new_file, io.BytesIO):
|
235
|
-
new_file.write(new_data)
|
236
|
-
new_file.seek(0)
|
237
|
-
elif new_file:
|
238
|
-
with open(new_file, "wb+") as f:
|
239
|
-
f.write(new_data)
|
240
|
-
elif output_is_file:
|
241
|
-
with open(src, "wb+") as f:
|
242
|
-
f.write(new_data)
|
243
|
-
else:
|
244
|
-
raise ValueError("Give a second argument to 'remove' to output file")
|
249
|
+
def removeExif(src, optimize=False):
|
250
|
+
if optimize:
|
251
|
+
optimizeFile(src)
|
245
252
|
|
253
|
+
with open(src, 'rb') as f:
|
254
|
+
src_data = f.read()
|
246
255
|
|
247
|
-
|
248
|
-
""
|
249
|
-
|
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))
|
250
260
|
|
251
|
-
|
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)
|
252
271
|
|
253
|
-
:param str filename1: JPEG
|
254
|
-
:param str filename2: JPEG
|
255
|
-
"""
|
256
272
|
if exif_src[0:2] == b"\xff\xd8":
|
257
273
|
src_data = exif_src
|
258
274
|
else:
|
@@ -262,41 +278,29 @@ def transplant(exif_src, image, new_file=None):
|
|
262
278
|
segments = genSegments(src_data)
|
263
279
|
exif = getExif(segments)
|
264
280
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
output_file = False
|
269
|
-
if image[0:2] == b"\xff\xd8":
|
270
|
-
image_data = image
|
271
|
-
else:
|
272
|
-
with open(image, 'rb') as f:
|
273
|
-
image_data = f.read()
|
274
|
-
output_file = True
|
281
|
+
with open(image, 'rb') as f:
|
282
|
+
image_data = f.read()
|
275
283
|
|
276
284
|
segments = genSegments(image_data)
|
277
285
|
segments = setComment(segments, "nt25.et")
|
278
286
|
new_data = mergeSegments(segments, exif)
|
279
287
|
|
280
|
-
|
281
|
-
|
282
|
-
new_file.seek(0)
|
283
|
-
elif new_file:
|
284
|
-
with open(new_file, "wb+") as f:
|
285
|
-
f.write(new_data)
|
286
|
-
elif output_file:
|
287
|
-
with open(image, "wb+") as f:
|
288
|
-
f.write(new_data)
|
289
|
-
else:
|
290
|
-
raise ValueError("Give a 3rd argument to 'transplant' to output file")
|
288
|
+
with open(image, "wb+") as f:
|
289
|
+
f.write(new_data)
|
291
290
|
|
292
291
|
|
293
292
|
def main():
|
294
293
|
parser = argparse.ArgumentParser(description="EXIF tool")
|
295
|
-
parser.add_argument('-v', '--version',
|
296
|
-
help='echo version'
|
297
|
-
parser.add_argument('-
|
298
|
-
|
299
|
-
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')
|
300
304
|
parser.add_argument('-f', '--file', type=str, help='image file')
|
301
305
|
|
302
306
|
args = parser.parse_args()
|
@@ -306,17 +310,20 @@ def main():
|
|
306
310
|
return
|
307
311
|
|
308
312
|
if args.file is None:
|
309
|
-
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]")
|
310
315
|
return
|
311
316
|
|
317
|
+
opt = True if args.optimize else False
|
318
|
+
|
312
319
|
if args.dump:
|
313
|
-
r = dumpExif(args.file)
|
320
|
+
r = dumpExif(args.file, optimize=opt)
|
314
321
|
elif args.rm:
|
315
|
-
r = removeExif(args.file)
|
322
|
+
r = removeExif(args.file, optimize=opt)
|
316
323
|
elif args.copy:
|
317
|
-
r = transplant(args.copy, args.file)
|
324
|
+
r = transplant(args.copy, args.file, optimize=opt)
|
318
325
|
else:
|
319
|
-
r = parseExif(args.file)
|
326
|
+
r = parseExif(args.file, optimize=opt)
|
320
327
|
|
321
328
|
if r is not None:
|
322
329
|
print(json.dumps(r, indent=2, sort_keys=False))
|
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
|
File without changes
|
File without changes
|