nt25 0.1.4__py3-none-any.whl → 0.1.5__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/__init__.py +3 -2
- nt25/{main.py → demo.py} +5 -2
- nt25/lib/et.py +209 -18
- {nt25-0.1.4.dist-info → nt25-0.1.5.dist-info}/METADATA +3 -2
- nt25-0.1.5.dist-info/RECORD +11 -0
- {nt25-0.1.4.dist-info → nt25-0.1.5.dist-info}/entry_points.txt +1 -1
- nt25-0.1.4.dist-info/RECORD +0 -11
- {nt25-0.1.4.dist-info → nt25-0.1.5.dist-info}/WHEEL +0 -0
nt25/__init__.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
import importlib.metadata as meta
|
2
2
|
|
3
|
-
from .lib import fio, calc, draw
|
3
|
+
from .lib import fio, calc, draw, et
|
4
4
|
from .lib.draw import DType
|
5
5
|
|
6
6
|
__version__ = meta.version(str(__package__))
|
7
7
|
__data_path__ = __file__.replace('__init__.py', 'data')
|
8
8
|
|
9
|
-
__all__ = ('__version__', '__data_path__',
|
9
|
+
__all__ = ('__version__', '__data_path__',
|
10
|
+
'fio', 'calc', 'draw', 'DType', 'et')
|
nt25/{main.py → demo.py}
RENAMED
@@ -1,5 +1,5 @@
|
|
1
1
|
import os
|
2
|
-
from nt25 import fio, calc, draw, DType, __version__, __data_path__
|
2
|
+
from nt25 import fio, calc, draw, DType, et, __version__, __data_path__
|
3
3
|
|
4
4
|
# import timeit
|
5
5
|
# timeit.timeit('', number=100, globals=globals())
|
@@ -70,7 +70,10 @@ def main():
|
|
70
70
|
bar = calc.solveEq(foo['eq'], output=True)
|
71
71
|
|
72
72
|
if len(bar) > 0:
|
73
|
-
print(f'solveEq(750, 1.5) ~ {bar[0]['func'](y=[750], x1=1.5):.4f}')
|
73
|
+
print(f'solveEq(750, 1.5) ~ {bar[0]['func'](y=[750], x1=1.5):.4f}\n')
|
74
|
+
|
75
|
+
exif = et.parseExif(__data_path__ + '/exif.jpg')
|
76
|
+
print('exif:', exif)
|
74
77
|
|
75
78
|
|
76
79
|
if __name__ == "__main__":
|
nt25/lib/et.py
CHANGED
@@ -1,16 +1,14 @@
|
|
1
|
+
import io
|
1
2
|
import time
|
2
3
|
import json
|
4
|
+
import struct
|
3
5
|
import argparse
|
4
6
|
|
5
7
|
from datetime import UTC, datetime, timedelta, timezone
|
6
|
-
from exif import Image
|
7
|
-
|
8
|
-
VERSION = "0.1.1"
|
8
|
+
from exif import Image, DATETIME_STR_FORMAT
|
9
9
|
|
10
|
+
VERSION = "0.1.2"
|
10
11
|
EPOCH = datetime.fromtimestamp(0, UTC)
|
11
|
-
IGNORE = ['orientation', 'maker_note', '_interoperability_ifd_Pointer',
|
12
|
-
'components_configuration', 'scene_type', 'flashpix_version',
|
13
|
-
'gps_processing_method',]
|
14
12
|
|
15
13
|
|
16
14
|
def dms2dec(dms: tuple):
|
@@ -19,11 +17,11 @@ def dms2dec(dms: tuple):
|
|
19
17
|
|
20
18
|
|
21
19
|
def dtFormatter(str):
|
22
|
-
return datetime.strptime(str,
|
20
|
+
return datetime.strptime(str, DATETIME_STR_FORMAT)
|
23
21
|
|
24
22
|
|
25
23
|
def dt2str(dt):
|
26
|
-
return None if dt is None else dt.strftime(
|
24
|
+
return None if dt is None else dt.strftime(DATETIME_STR_FORMAT)
|
27
25
|
|
28
26
|
|
29
27
|
def gpsDt2Dt(date, time, offset=8):
|
@@ -73,13 +71,6 @@ def parseExif(file):
|
|
73
71
|
create = tryGet(img, 'datetime_original', None)
|
74
72
|
modify = tryGet(img, 'datetime', None)
|
75
73
|
|
76
|
-
# da = []
|
77
|
-
# for d in (d1, d2, d3):
|
78
|
-
# if d is not None:
|
79
|
-
# print(d)
|
80
|
-
# da.append(dtFormatter(d))
|
81
|
-
# dt = None if len(da) == 0 else max(da)
|
82
|
-
|
83
74
|
createDt = None if create is None else dtFormatter(create)
|
84
75
|
modifyDt = None if modify is None else dtFormatter(modify)
|
85
76
|
|
@@ -117,11 +108,202 @@ def parseExif(file):
|
|
117
108
|
}
|
118
109
|
|
119
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
|
+
|
120
300
|
def main():
|
121
301
|
parser = argparse.ArgumentParser(description="EXIF tool")
|
122
302
|
parser.add_argument('-v', '--version',
|
123
303
|
help='echo version', action='store_true')
|
124
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')
|
125
307
|
parser.add_argument('-f', '--file', type=str, help='image file')
|
126
308
|
|
127
309
|
args = parser.parse_args()
|
@@ -131,11 +313,20 @@ def main():
|
|
131
313
|
return
|
132
314
|
|
133
315
|
if args.file is None:
|
134
|
-
print("usage: et [-h] [-v] [-d] [-f FILE]")
|
316
|
+
print("usage: et [-h] [-v] [-r FILE] [-c FILE] [-d] [-f FILE]")
|
135
317
|
return
|
136
318
|
|
137
|
-
|
138
|
-
|
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))
|
139
330
|
|
140
331
|
|
141
332
|
if __name__ == "__main__":
|
@@ -1,12 +1,13 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: nt25
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.5
|
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: piexif>=1.1.3
|
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
|
@@ -32,7 +33,7 @@ Neo's Tools of Python in 2025
|
|
32
33
|
```sh
|
33
34
|
uv init
|
34
35
|
# fio, calc, draw basic demo
|
35
|
-
uv run
|
36
|
+
uv run demo
|
36
37
|
# et
|
37
38
|
uv run et
|
38
39
|
```
|
@@ -0,0 +1,11 @@
|
|
1
|
+
nt25/__init__.py,sha256=28wWlyuyScDrZx9ytGM_TxUipk5dg4pOyp_xyj6-NWk,295
|
2
|
+
nt25/demo.py,sha256=W34-7KTYVpBYXg0UrgmkVCRWAdO5RAc1K7zS9pz-BEQ,2179
|
3
|
+
nt25/data/test.xlsx,sha256=7C0JDS-TLm_KmjnKtfeajkpwGKSUhcLdr2W2UFUxAgM,10542
|
4
|
+
nt25/lib/calc.py,sha256=3X3k9jisSjRP7OokSdKvoVo4IIOzk2efexW8z1gMo-w,2265
|
5
|
+
nt25/lib/draw.py,sha256=OKTlkkNVUz_LGBA9Gk7fjcnbbbl7e_hT8nWKkcfeg2k,5642
|
6
|
+
nt25/lib/et.py,sha256=BWg4dl1EuDA6pKQbWqxSY_OE5ya7l8vvGsMb7SGDpaM,7933
|
7
|
+
nt25/lib/fio.py,sha256=WvHpG6QYR1NE19Ss3Sy2FdajTxibX5SVW3PyC5Y5Krk,2525
|
8
|
+
nt25-0.1.5.dist-info/METADATA,sha256=NbRanYnyhkstfCxQIS-nxf2f1E4VdI_QZJhd0TBmmeM,683
|
9
|
+
nt25-0.1.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
+
nt25-0.1.5.dist-info/entry_points.txt,sha256=mtt7YI92CecNLeMvyM3o5IyVzRpYSqqwmUgAzldhFH8,62
|
11
|
+
nt25-0.1.5.dist-info/RECORD,,
|
nt25-0.1.4.dist-info/RECORD
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
nt25/__init__.py,sha256=oIVhU2wegSHArUmypHr3nXQa8RRtH5TBZgRdRNN-OEw,274
|
2
|
-
nt25/main.py,sha256=nVzuGx8PaxDU-QbLmVtAvDkfEFX5Z5g0xE0CWf9EwBA,2098
|
3
|
-
nt25/data/test.xlsx,sha256=7C0JDS-TLm_KmjnKtfeajkpwGKSUhcLdr2W2UFUxAgM,10542
|
4
|
-
nt25/lib/calc.py,sha256=3X3k9jisSjRP7OokSdKvoVo4IIOzk2efexW8z1gMo-w,2265
|
5
|
-
nt25/lib/draw.py,sha256=OKTlkkNVUz_LGBA9Gk7fjcnbbbl7e_hT8nWKkcfeg2k,5642
|
6
|
-
nt25/lib/et.py,sha256=X_w-f2yb2jPtauPZKfbonDH4FhNFVOiFwZBp9yga8HE,3643
|
7
|
-
nt25/lib/fio.py,sha256=WvHpG6QYR1NE19Ss3Sy2FdajTxibX5SVW3PyC5Y5Krk,2525
|
8
|
-
nt25-0.1.4.dist-info/METADATA,sha256=DbMNv3C3BKdJ0yD2iQg7ii3-pQw3qEDFbjUZqJE8MLo,654
|
9
|
-
nt25-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
-
nt25-0.1.4.dist-info/entry_points.txt,sha256=6O4CnhT__4-ORTKEY9vV-MjSzJwDM4FFbVODQsN4Gr8,62
|
11
|
-
nt25-0.1.4.dist-info/RECORD,,
|
File without changes
|