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.2"
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
- width = tryGet(img, 'pixel_x_dimension', -1)
65
- height = tryGet(img, 'pixel_y_dimension', -1)
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
- if width < 0:
68
- width = tryGet(img, 'image_width', -1)
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
- create = tryGet(img, 'datetime_original', None)
72
- modify = tryGet(img, 'datetime', None)
103
+ latitude = tryGet(img, 'gps_latitude', None)
104
+ latitude = None if latitude is None else dms2dec(latitude)
73
105
 
74
- createDt = None if create is None else dtFormatter(create)
75
- modifyDt = None if modify is None else dtFormatter(modify)
106
+ latRef = tryGet(img, "gps_latitude_ref", default='N')
107
+ if latRef != 'N' and latitude:
108
+ latitude = -latitude
76
109
 
77
- latitude = tryGet(img, 'gps_latitude', None)
78
- latitude = None if latitude is None else dms2dec(latitude)
110
+ longitude = tryGet(img, 'gps_longitude', None)
111
+ longitude = None if longitude is None else dms2dec(longitude)
79
112
 
80
- longitude = tryGet(img, 'gps_longitude', None)
81
- longitude = None if longitude is None else dms2dec(longitude)
113
+ longRef = tryGet(img, "gps_longitude_ref", default='E')
114
+ if longRef != 'E' and longitude:
115
+ longitude = -longitude
82
116
 
83
- gpsDatetime = None
84
- gd = tryGet(img, 'gps_datestamp', None)
85
- gt = tryGet(img, 'gps_timestamp', None)
117
+ gpsDatetime = None
118
+ gd = tryGet(img, 'gps_datestamp', None)
119
+ gt = tryGet(img, 'gps_timestamp', None)
86
120
 
87
- if gd and gt:
88
- offset = int(time.localtime().tm_gmtoff / 3600)
89
- gpsDatetime = gpsDt2Dt(gd, gt, offset=offset)
121
+ if gd and gt:
122
+ offset = int(time.localtime().tm_gmtoff / 3600)
123
+ gpsDatetime = gpsDt2Dt(gd, gt, offset=offset)
90
124
 
91
- ts = -1 if createDt is None else int(createDt.timestamp())
92
- mTs = -1 if modifyDt is None else int(modifyDt.timestamp())
93
- gpsTs = -1 if gpsDatetime is None else int(gpsDatetime.timestamp())
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
- return {
98
- "width": width,
99
- "height": height,
100
- "latitude": latitude,
101
- "longitude": longitude,
102
- "datetime.create": dt2str(createDt),
103
- "datetime.modify": dt2str(modifyDt),
104
- "datetime.gps": dt2str(gpsDatetime),
105
- "ts": ts,
106
- "offset": offset,
107
- "offset.delta": str(offsetDelta),
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 split_into_segments(data):
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 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)
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
- if data[0:2] != b"\xff\xd8":
147
- raise InvalidImageDataError("Given data isn't JPEG.")
184
+ cbSeg = COMMENT_SEGMENT + length.to_bytes(2, byteorder='big') + cb
148
185
 
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
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
- f.close()
169
- return exif
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
- def get_exif_seg(segments):
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 None
213
+ return b""
180
214
 
181
215
 
182
- def merge_segments(segments, exif=b""):
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 remove(src, new_file=None):
216
- """
217
- py:function:: piexif.remove(filename)
249
+ def removeExif(src, optimize=False):
250
+ if optimize:
251
+ optimizeFile(src)
218
252
 
219
- Remove exif from JPEG.
253
+ with open(src, 'rb') as f:
254
+ src_data = f.read()
220
255
 
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"
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
- 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")
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
- Transplant exif from filename1 to filename2.
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 = split_into_segments(src_data)
271
- exif = get_exif_seg(segments)
278
+ segments = genSegments(src_data)
279
+ exif = getExif(segments)
272
280
 
273
- if exif is None:
274
- raise ValueError("not found exif in input")
281
+ with open(image, 'rb') as f:
282
+ image_data = f.read()
275
283
 
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")
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', action='store_true')
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')
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] [-r FILE] [-c FILE] [-d] [-f FILE]")
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 = remove(args.file)
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.5
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: piexif>=1.1.3
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=BWg4dl1EuDA6pKQbWqxSY_OE5ya7l8vvGsMb7SGDpaM,7933
7
+ nt25/lib/et.py,sha256=uhTwNTM3NRElxvw6jiMHjmg6vrS5K2Fj3mj-YjQs_iM,7873
7
8
  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,,
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