nt25 0.1.4__py3-none-any.whl → 0.1.6__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 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__', 'fio', 'calc', 'draw', 'DType')
9
+ __all__ = ('__version__', '__data_path__',
10
+ 'fio', 'calc', 'draw', 'DType', 'et')
nt25/data/exif.jpg ADDED
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__":
nt25/lib/et.py CHANGED
@@ -1,16 +1,16 @@
1
+ import io
2
+ from operator import length_hint
1
3
  import time
2
4
  import json
5
+ import struct
3
6
  import argparse
4
7
 
5
8
  from datetime import UTC, datetime, timedelta, timezone
6
- from exif import Image
7
-
8
- VERSION = "0.1.1"
9
+ from exif import Image, DATETIME_STR_FORMAT
9
10
 
11
+ VERSION = "0.1.2"
12
+ COMMENT_SEGMENT = b"\xff\xfe"
10
13
  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
14
 
15
15
 
16
16
  def dms2dec(dms: tuple):
@@ -19,11 +19,11 @@ def dms2dec(dms: tuple):
19
19
 
20
20
 
21
21
  def dtFormatter(str):
22
- return datetime.strptime(str, '%Y:%m:%d %H:%M:%S')
22
+ return datetime.strptime(str, DATETIME_STR_FORMAT)
23
23
 
24
24
 
25
25
  def dt2str(dt):
26
- return None if dt is None else dt.strftime('%Y-%m-%d %H:%M:%S')
26
+ return None if dt is None else dt.strftime(DATETIME_STR_FORMAT)
27
27
 
28
28
 
29
29
  def gpsDt2Dt(date, time, offset=8):
@@ -45,6 +45,7 @@ def tryGet(img, key, default):
45
45
 
46
46
  def dumpExif(file):
47
47
  result = {}
48
+
48
49
  with open(file, 'rb') as f:
49
50
  img = Image(f)
50
51
  for key in img.get_all():
@@ -73,13 +74,6 @@ def parseExif(file):
73
74
  create = tryGet(img, 'datetime_original', None)
74
75
  modify = tryGet(img, 'datetime', None)
75
76
 
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
77
  createDt = None if create is None else dtFormatter(create)
84
78
  modifyDt = None if modify is None else dtFormatter(modify)
85
79
 
@@ -117,11 +111,192 @@ def parseExif(file):
117
111
  }
118
112
 
119
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
+
120
293
  def main():
121
294
  parser = argparse.ArgumentParser(description="EXIF tool")
122
295
  parser.add_argument('-v', '--version',
123
296
  help='echo version', action='store_true')
124
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')
125
300
  parser.add_argument('-f', '--file', type=str, help='image file')
126
301
 
127
302
  args = parser.parse_args()
@@ -131,11 +306,20 @@ def main():
131
306
  return
132
307
 
133
308
  if args.file is None:
134
- print("usage: et [-h] [-v] [-d] [-f FILE]")
309
+ print("usage: et [-h] [-v] [-r FILE] [-c FILE] [-d] [-f FILE]")
135
310
  return
136
311
 
137
- r = dumpExif(args.file) if args.dump else parseExif(args.file)
138
- print(json.dumps(r, indent=2, sort_keys=False))
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))
139
323
 
140
324
 
141
325
  if __name__ == "__main__":
@@ -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
  ```
@@ -0,0 +1,12 @@
1
+ nt25/__init__.py,sha256=28wWlyuyScDrZx9ytGM_TxUipk5dg4pOyp_xyj6-NWk,295
2
+ nt25/demo.py,sha256=W34-7KTYVpBYXg0UrgmkVCRWAdO5RAc1K7zS9pz-BEQ,2179
3
+ nt25/data/exif.jpg,sha256=Wl9QqZ3UAC_ldd055lJOcZKqvU5swqLEw_HxRBbLYgc,347767
4
+ nt25/data/test.xlsx,sha256=7C0JDS-TLm_KmjnKtfeajkpwGKSUhcLdr2W2UFUxAgM,10542
5
+ nt25/lib/calc.py,sha256=3X3k9jisSjRP7OokSdKvoVo4IIOzk2efexW8z1gMo-w,2265
6
+ nt25/lib/draw.py,sha256=OKTlkkNVUz_LGBA9Gk7fjcnbbbl7e_hT8nWKkcfeg2k,5642
7
+ nt25/lib/et.py,sha256=b7inPmDy-Vj0v7hdJdpCV-K5c05BgJpVzKAWJe75tfE,7873
8
+ nt25/lib/fio.py,sha256=WvHpG6QYR1NE19Ss3Sy2FdajTxibX5SVW3PyC5Y5Krk,2525
9
+ nt25-0.1.6.dist-info/METADATA,sha256=LV7UAehxnrZbCOXjbivjfT-xHC9StucF_5xmgb5V4c0,654
10
+ nt25-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
+ nt25-0.1.6.dist-info/entry_points.txt,sha256=mtt7YI92CecNLeMvyM3o5IyVzRpYSqqwmUgAzldhFH8,62
12
+ nt25-0.1.6.dist-info/RECORD,,
@@ -1,3 +1,3 @@
1
1
  [console_scripts]
2
+ demo = nt25.demo:main
2
3
  et = nt25.lib.et:main
3
- nt25 = nt25.main:main
@@ -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