nt25 0.1.4__tar.gz → 0.1.6__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.
@@ -20,6 +20,6 @@ ds/
20
20
  output/
21
21
 
22
22
  # others
23
- *.jpg
24
- *.png
23
+ tests/*.jpg
24
+ tests/*.png
25
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nt25
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Neo's Tools of Python
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: exif>=1.6.1
@@ -32,7 +32,7 @@ Neo's Tools of Python in 2025
32
32
  ```sh
33
33
  uv init
34
34
  # fio, calc, draw basic demo
35
- uv run nt25
35
+ uv run demo
36
36
  # et
37
37
  uv run et
38
38
  ```
@@ -18,7 +18,7 @@ Neo's Tools of Python in 2025
18
18
  ```sh
19
19
  uv init
20
20
  # fio, calc, draw basic demo
21
- uv run nt25
21
+ uv run demo
22
22
  # et
23
23
  uv run et
24
24
  ```
@@ -1,21 +1,21 @@
1
1
  [project]
2
2
  name = "nt25"
3
- version = "0.1.4"
3
+ version = "0.1.6"
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
- "pyinstaller>=6.15.0",
12
+ "openpyxl>=3.1.5",
13
13
  "scikit-learn>=1.7.1",
14
14
  "sympy>=1.14.0",
15
15
  ]
16
16
 
17
17
  [project.scripts]
18
- nt25 = "nt25.main:main"
18
+ demo = "nt25.demo:main"
19
19
  et = "nt25.lib.et:main"
20
20
 
21
21
  [build-system]
@@ -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__', 'fio', 'calc', 'draw', 'DType')
9
+ __all__ = ('__version__', '__data_path__',
10
+ 'fio', 'calc', 'draw', 'DType', 'et')
Binary file
@@ -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__":
@@ -0,0 +1,326 @@
1
+ import io
2
+ from operator import length_hint
3
+ import time
4
+ import json
5
+ import struct
6
+ import argparse
7
+
8
+ from datetime import UTC, datetime, timedelta, timezone
9
+ from exif import Image, DATETIME_STR_FORMAT
10
+
11
+ VERSION = "0.1.2"
12
+ COMMENT_SEGMENT = b"\xff\xfe"
13
+ EPOCH = datetime.fromtimestamp(0, UTC)
14
+
15
+
16
+ def dms2dec(dms: tuple):
17
+ d, m, s = dms
18
+ return d + m/60 + s/3600
19
+
20
+
21
+ def dtFormatter(str):
22
+ return datetime.strptime(str, DATETIME_STR_FORMAT)
23
+
24
+
25
+ def dt2str(dt):
26
+ return None if dt is None else dt.strftime(DATETIME_STR_FORMAT)
27
+
28
+
29
+ def gpsDt2Dt(date, time, offset=8):
30
+ d = dtFormatter(f"{date} {int(time[0])}:{int(time[1])}:{int(time[2])}")
31
+ utc = d.replace(tzinfo=timezone.utc)
32
+ return utc.astimezone(timezone(timedelta(hours=offset)))
33
+
34
+
35
+ def tryGet(img, key, default):
36
+ value = default
37
+
38
+ try:
39
+ value = img[key]
40
+ except Exception:
41
+ pass
42
+
43
+ return value
44
+
45
+
46
+ def dumpExif(file):
47
+ result = {}
48
+
49
+ with open(file, 'rb') as f:
50
+ img = Image(f)
51
+ for key in img.get_all():
52
+ try:
53
+ result[key] = str(img[key])
54
+ except Exception:
55
+ pass
56
+
57
+ return result
58
+
59
+
60
+ def parseExif(file):
61
+ with open(file, 'rb') as f:
62
+ try:
63
+ img = Image(f)
64
+ except Exception:
65
+ return {}
66
+
67
+ width = tryGet(img, 'pixel_x_dimension', -1)
68
+ height = tryGet(img, 'pixel_y_dimension', -1)
69
+
70
+ if width < 0:
71
+ width = tryGet(img, 'image_width', -1)
72
+ height = tryGet(img, 'image_height', -1)
73
+
74
+ create = tryGet(img, 'datetime_original', None)
75
+ modify = tryGet(img, 'datetime', None)
76
+
77
+ createDt = None if create is None else dtFormatter(create)
78
+ modifyDt = None if modify is None else dtFormatter(modify)
79
+
80
+ latitude = tryGet(img, 'gps_latitude', None)
81
+ latitude = None if latitude is None else dms2dec(latitude)
82
+
83
+ longitude = tryGet(img, 'gps_longitude', None)
84
+ longitude = None if longitude is None else dms2dec(longitude)
85
+
86
+ gpsDatetime = None
87
+ gd = tryGet(img, 'gps_datestamp', None)
88
+ gt = tryGet(img, 'gps_timestamp', None)
89
+
90
+ if gd and gt:
91
+ offset = int(time.localtime().tm_gmtoff / 3600)
92
+ gpsDatetime = gpsDt2Dt(gd, gt, offset=offset)
93
+
94
+ ts = -1 if createDt is None else int(createDt.timestamp())
95
+ mTs = -1 if modifyDt is None else int(modifyDt.timestamp())
96
+ gpsTs = -1 if gpsDatetime is None else int(gpsDatetime.timestamp())
97
+ offset = max(mTs, gpsTs) - ts
98
+ offsetDelta = datetime.fromtimestamp(offset, UTC) - EPOCH
99
+
100
+ return {
101
+ "width": width,
102
+ "height": height,
103
+ "latitude": latitude,
104
+ "longitude": longitude,
105
+ "datetime.create": dt2str(createDt),
106
+ "datetime.modify": dt2str(modifyDt),
107
+ "datetime.gps": dt2str(gpsDatetime),
108
+ "ts": ts,
109
+ "offset": offset,
110
+ "offset.delta": str(offsetDelta),
111
+ }
112
+
113
+
114
+ class InvalidImageDataError(ValueError):
115
+ pass
116
+
117
+
118
+ def genSegments(data):
119
+ if data[0:2] != b"\xff\xd8":
120
+ raise InvalidImageDataError("Given data isn't JPEG.")
121
+
122
+ head = 2
123
+ segments = [b"\xff\xd8"]
124
+
125
+ while 1:
126
+ if data[head: head + 2] == b"\xff\xda":
127
+ segments.append(data[head:])
128
+ break
129
+
130
+ else:
131
+ length = struct.unpack(">H", data[head + 2: head + 4])[0]
132
+ endPoint = head + length + 2
133
+ seg = data[head: endPoint]
134
+ segments.append(seg)
135
+ head = endPoint
136
+
137
+ if (head >= len(data)):
138
+ raise InvalidImageDataError("Wrong JPEG data.")
139
+
140
+ return segments
141
+
142
+
143
+ def setComment(segments, comment: str, enc='utf-8'):
144
+ contains = False
145
+ cb = comment.encode(enc)
146
+ length = len(cb) + 2
147
+
148
+ cbSeg = COMMENT_SEGMENT + length.to_bytes(2, byteorder='big') + cb
149
+
150
+ for i in range(len(segments)):
151
+ if segments[i][0:2] == COMMENT_SEGMENT:
152
+ contains = True
153
+ segments[i] = cbSeg
154
+
155
+ if not contains:
156
+ length = len(segments)
157
+ segments.insert(1 if length == 2 else length - 2, cbSeg)
158
+
159
+ return segments
160
+
161
+
162
+ def getComment(segments, enc='utf-8'):
163
+ for seg in segments:
164
+ if seg[0:2] == COMMENT_SEGMENT:
165
+ return seg[4:].decode(encoding=enc, errors='replace')
166
+
167
+ return None
168
+
169
+
170
+ def getExif(segments):
171
+ """Returns Exif from JPEG meta data list
172
+ """
173
+ for seg in segments:
174
+ if seg[0:2] == b"\xff\xe1" and seg[4:10] == b"Exif\x00\x00":
175
+ return seg
176
+
177
+ return None
178
+
179
+
180
+ def mergeSegments(segments, exif=b""):
181
+ """Merges Exif with APP0 and APP1 manipulations.
182
+ """
183
+ if segments[1][0:2] == b"\xff\xe0" and \
184
+ segments[2][0:2] == b"\xff\xe1" and \
185
+ segments[2][4:10] == b"Exif\x00\x00":
186
+ if exif:
187
+ segments[2] = exif
188
+ segments.pop(1)
189
+ elif exif is None:
190
+ segments.pop(2)
191
+ else:
192
+ segments.pop(1)
193
+
194
+ elif segments[1][0:2] == b"\xff\xe0":
195
+ if exif:
196
+ segments[1] = exif
197
+
198
+ elif (segments[1][0:2] == b"\xff\xe1" and
199
+ segments[1][4:10] == b"Exif\x00\x00"):
200
+
201
+ if exif:
202
+ segments[1] = exif
203
+ elif exif is None:
204
+ segments.pop(1)
205
+
206
+ else:
207
+ if exif:
208
+ segments.insert(1, exif)
209
+
210
+ return b"".join(segments)
211
+
212
+
213
+ def removeExif(src, new_file=None):
214
+ output_is_file = False
215
+ if src[0:2] == b"\xff\xd8":
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")
245
+
246
+
247
+ def transplant(exif_src, image, new_file=None):
248
+ """
249
+ py:function:: piexif.transplant(filename1, filename2)
250
+
251
+ Transplant exif from filename1 to filename2.
252
+
253
+ :param str filename1: JPEG
254
+ :param str filename2: JPEG
255
+ """
256
+ if exif_src[0:2] == b"\xff\xd8":
257
+ src_data = exif_src
258
+ else:
259
+ with open(exif_src, 'rb') as f:
260
+ src_data = f.read()
261
+
262
+ segments = genSegments(src_data)
263
+ exif = getExif(segments)
264
+
265
+ if exif is None:
266
+ raise ValueError("not found exif in input")
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
275
+
276
+ segments = genSegments(image_data)
277
+ segments = setComment(segments, "nt25.et")
278
+ new_data = mergeSegments(segments, exif)
279
+
280
+ if isinstance(new_file, io.BytesIO):
281
+ new_file.write(new_data)
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")
291
+
292
+
293
+ def main():
294
+ parser = argparse.ArgumentParser(description="EXIF tool")
295
+ parser.add_argument('-v', '--version',
296
+ help='echo version', action='store_true')
297
+ parser.add_argument('-d', '--dump', help='dump meta', action='store_true')
298
+ parser.add_argument('-r', '--rm', help='remove meta', action='store_true')
299
+ parser.add_argument('-c', '--copy', type=str, help='copy meta')
300
+ parser.add_argument('-f', '--file', type=str, help='image file')
301
+
302
+ args = parser.parse_args()
303
+
304
+ if args.version:
305
+ print(f"et: {VERSION}")
306
+ return
307
+
308
+ if args.file is None:
309
+ print("usage: et [-h] [-v] [-r FILE] [-c FILE] [-d] [-f FILE]")
310
+ return
311
+
312
+ if args.dump:
313
+ r = dumpExif(args.file)
314
+ elif args.rm:
315
+ r = removeExif(args.file)
316
+ elif args.copy:
317
+ r = transplant(args.copy, args.file)
318
+ else:
319
+ r = parseExif(args.file)
320
+
321
+ if r is not None:
322
+ print(json.dumps(r, indent=2, sort_keys=False))
323
+
324
+
325
+ if __name__ == "__main__":
326
+ main()
@@ -1,142 +0,0 @@
1
- import time
2
- import json
3
- import argparse
4
-
5
- from datetime import UTC, datetime, timedelta, timezone
6
- from exif import Image
7
-
8
- VERSION = "0.1.1"
9
-
10
- 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
-
15
-
16
- def dms2dec(dms: tuple):
17
- d, m, s = dms
18
- return d + m/60 + s/3600
19
-
20
-
21
- def dtFormatter(str):
22
- return datetime.strptime(str, '%Y:%m:%d %H:%M:%S')
23
-
24
-
25
- def dt2str(dt):
26
- return None if dt is None else dt.strftime('%Y-%m-%d %H:%M:%S')
27
-
28
-
29
- def gpsDt2Dt(date, time, offset=8):
30
- d = dtFormatter(f"{date} {int(time[0])}:{int(time[1])}:{int(time[2])}")
31
- utc = d.replace(tzinfo=timezone.utc)
32
- return utc.astimezone(timezone(timedelta(hours=offset)))
33
-
34
-
35
- def tryGet(img, key, default):
36
- value = default
37
-
38
- try:
39
- value = img[key]
40
- except Exception:
41
- pass
42
-
43
- return value
44
-
45
-
46
- def dumpExif(file):
47
- result = {}
48
- with open(file, 'rb') as f:
49
- img = Image(f)
50
- for key in img.get_all():
51
- try:
52
- result[key] = str(img[key])
53
- except Exception:
54
- pass
55
-
56
- return result
57
-
58
-
59
- def parseExif(file):
60
- with open(file, 'rb') as f:
61
- try:
62
- img = Image(f)
63
- except Exception:
64
- return {}
65
-
66
- width = tryGet(img, 'pixel_x_dimension', -1)
67
- height = tryGet(img, 'pixel_y_dimension', -1)
68
-
69
- if width < 0:
70
- width = tryGet(img, 'image_width', -1)
71
- height = tryGet(img, 'image_height', -1)
72
-
73
- create = tryGet(img, 'datetime_original', None)
74
- modify = tryGet(img, 'datetime', None)
75
-
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
- createDt = None if create is None else dtFormatter(create)
84
- modifyDt = None if modify is None else dtFormatter(modify)
85
-
86
- latitude = tryGet(img, 'gps_latitude', None)
87
- latitude = None if latitude is None else dms2dec(latitude)
88
-
89
- longitude = tryGet(img, 'gps_longitude', None)
90
- longitude = None if longitude is None else dms2dec(longitude)
91
-
92
- gpsDatetime = None
93
- gd = tryGet(img, 'gps_datestamp', None)
94
- gt = tryGet(img, 'gps_timestamp', None)
95
-
96
- if gd and gt:
97
- offset = int(time.localtime().tm_gmtoff / 3600)
98
- gpsDatetime = gpsDt2Dt(gd, gt, offset=offset)
99
-
100
- ts = -1 if createDt is None else int(createDt.timestamp())
101
- mTs = -1 if modifyDt is None else int(modifyDt.timestamp())
102
- gpsTs = -1 if gpsDatetime is None else int(gpsDatetime.timestamp())
103
- offset = max(mTs, gpsTs) - ts
104
- offsetDelta = datetime.fromtimestamp(offset, UTC) - EPOCH
105
-
106
- return {
107
- "width": width,
108
- "height": height,
109
- "latitude": latitude,
110
- "longitude": longitude,
111
- "datetime.create": dt2str(createDt),
112
- "datetime.modify": dt2str(modifyDt),
113
- "datetime.gps": dt2str(gpsDatetime),
114
- "ts": ts,
115
- "offset": offset,
116
- "offset.delta": str(offsetDelta),
117
- }
118
-
119
-
120
- def main():
121
- parser = argparse.ArgumentParser(description="EXIF tool")
122
- parser.add_argument('-v', '--version',
123
- help='echo version', action='store_true')
124
- parser.add_argument('-d', '--dump', help='dump meta', action='store_true')
125
- parser.add_argument('-f', '--file', type=str, help='image file')
126
-
127
- args = parser.parse_args()
128
-
129
- if args.version:
130
- print(f"et: {VERSION}")
131
- return
132
-
133
- if args.file is None:
134
- print("usage: et [-h] [-v] [-d] [-f FILE]")
135
- return
136
-
137
- r = dumpExif(args.file) if args.dump else parseExif(args.file)
138
- print(json.dumps(r, indent=2, sort_keys=False))
139
-
140
-
141
- if __name__ == "__main__":
142
- main()
nt25-0.1.4/tests/pack.sh DELETED
@@ -1,8 +0,0 @@
1
- #!env bash
2
- uvx pyinstaller \
3
- --hidden-import=matplotlib \
4
- --hidden-import=openpyxl \
5
- --hidden-import=pandas \
6
- --hidden-import=scikit-learn \
7
- --hidden-import=sympy \
8
- ./src/nt25/main.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes