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.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nt25
3
- Version: 0.1.6
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.6"
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
- from operator import length_hint
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.2"
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
- width = tryGet(img, 'pixel_x_dimension', -1)
68
- 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)
99
+
100
+ createDt = None if create is None else dtFormatter(create)
101
+ modifyDt = None if modify is None else dtFormatter(modify)
69
102
 
70
- if width < 0:
71
- width = tryGet(img, 'image_width', -1)
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
- create = tryGet(img, 'datetime_original', None)
75
- modify = tryGet(img, 'datetime', None)
106
+ latRef = tryGet(img, "gps_latitude_ref", default='N')
107
+ if latRef != 'N' and latitude:
108
+ latitude = -latitude
76
109
 
77
- createDt = None if create is None else dtFormatter(create)
78
- modifyDt = None if modify is None else dtFormatter(modify)
110
+ longitude = tryGet(img, 'gps_longitude', None)
111
+ longitude = None if longitude is None else dms2dec(longitude)
79
112
 
80
- latitude = tryGet(img, 'gps_latitude', None)
81
- latitude = None if latitude is None else dms2dec(latitude)
113
+ longRef = tryGet(img, "gps_longitude_ref", default='E')
114
+ if longRef != 'E' and longitude:
115
+ longitude = -longitude
82
116
 
83
- longitude = tryGet(img, 'gps_longitude', None)
84
- longitude = None if longitude is None else dms2dec(longitude)
117
+ gpsDatetime = None
118
+ gd = tryGet(img, 'gps_datestamp', None)
119
+ gt = tryGet(img, 'gps_timestamp', None)
85
120
 
86
- gpsDatetime = None
87
- gd = tryGet(img, 'gps_datestamp', None)
88
- gt = tryGet(img, 'gps_timestamp', None)
121
+ if gd and gt:
122
+ offset = int(time.localtime().tm_gmtoff / 3600)
123
+ gpsDatetime = gpsDt2Dt(gd, gt, offset=offset)
89
124
 
90
- if gd and gt:
91
- offset = int(time.localtime().tm_gmtoff / 3600)
92
- gpsDatetime = gpsDt2Dt(gd, gt, offset=offset)
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
- 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())
129
+ if ts > 0:
97
130
  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
- }
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 None
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, 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")
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
- def transplant(exif_src, image, new_file=None):
248
- """
249
- py:function:: piexif.transplant(filename1, filename2)
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
- Transplant exif from filename1 to filename2.
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
- 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
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
- 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")
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', 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')
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] [-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]")
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