sticker-convert 2.3.0__py3-none-any.whl → 2.4.0__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.
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ import unicodedata
3
+ import re
4
+
5
+ def sanitize_filename(filename: str) -> str:
6
+ # Based on https://gitlab.com/jplusplus/sanitize-filename/-/blob/master/sanitize_filename/sanitize_filename.py
7
+ # Replace illegal character with '_'
8
+ """Return a fairly safe version of the filename.
9
+
10
+ We don't limit ourselves to ascii, because we want to keep municipality
11
+ names, etc, but we do want to get rid of anything potentially harmful,
12
+ and make sure we do not exceed Windows filename length limits.
13
+ Hence a less safe blacklist, rather than a whitelist.
14
+ """
15
+ blacklist = ["\\", "/", ":", "*", "?", '"', "<", ">", "|", "\0"]
16
+ reserved = [
17
+ "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5",
18
+ "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5",
19
+ "LPT6", "LPT7", "LPT8", "LPT9",
20
+ ] # Reserved words on Windows
21
+ filename = "".join(c if c not in blacklist else "_" for c in filename)
22
+ # Remove all charcters below code point 32
23
+ filename = "".join(c if 31 < ord(c) else "_" for c in filename)
24
+ filename = unicodedata.normalize("NFKD", filename)
25
+ filename = filename.rstrip(". ") # Windows does not allow these at end
26
+ filename = filename.strip()
27
+ if all([x == "." for x in filename]):
28
+ filename = "__" + filename
29
+ if filename in reserved:
30
+ filename = "__" + filename
31
+ if len(filename) == 0:
32
+ filename = "__"
33
+ if len(filename) > 255:
34
+ parts = re.split(r"/|\\", filename)[-1].split(".")
35
+ if len(parts) > 1:
36
+ ext = "." + parts.pop()
37
+ filename = filename[: -len(ext)]
38
+ else:
39
+ ext = ""
40
+ if filename == "":
41
+ filename = "__"
42
+ if len(ext) > 254:
43
+ ext = ext[254:]
44
+ maxl = 255 - len(ext)
45
+ filename = filename[:maxl]
46
+ filename = filename + ext
47
+ # Re-check last character (if there was no extension)
48
+ filename = filename.rstrip(". ")
49
+ if len(filename) == 0:
50
+ filename = "__"
51
+ return filename
@@ -1,161 +1,227 @@
1
1
  #!/usr/bin/env python3
2
2
  from __future__ import annotations
3
3
  import os
4
- import mimetypes
4
+ import mmap
5
5
  from typing import Optional
6
6
 
7
- import imageio.v3 as iio
8
- import av # type: ignore
9
- from av.codec.context import CodecContext # type: ignore
10
- from rlottie_python import LottieAnimation # type: ignore
11
7
  from PIL import Image, UnidentifiedImageError
12
- import mmap
13
-
14
8
 
15
9
  class CodecInfo:
16
- def __init__(self):
17
- mimetypes.init()
18
- vid_ext = []
19
- for ext in mimetypes.types_map:
20
- if mimetypes.types_map[ext].split("/")[0] == "video":
21
- vid_ext.append(ext)
22
- vid_ext.append(".webm")
23
- vid_ext.append(".webp")
24
- self.vid_ext = tuple(vid_ext)
10
+ def __init__(self, file: str):
11
+ self.file_ext = CodecInfo.get_file_ext(file)
12
+ self.fps, self.frames, self.duration = CodecInfo.get_file_fps_frames_duration(file)
13
+ self.codec = CodecInfo.get_file_codec(file)
14
+ self.res = CodecInfo.get_file_res(file)
15
+ self.is_animated = True if self.fps > 1 else False
25
16
 
26
17
  @staticmethod
27
- def get_file_fps(file: str) -> float:
18
+ def get_file_fps_frames_duration(file: str) -> tuple[float, int, int]:
28
19
  file_ext = CodecInfo.get_file_ext(file)
29
20
 
30
- if file_ext in ".tgs":
31
- with LottieAnimation.from_tgs(file) as anim:
32
- fps = anim.lottie_animation_get_framerate()
21
+ if file_ext == ".tgs":
22
+ fps, frames = CodecInfo._get_file_fps_frames_tgs(file)
23
+ duration = int(frames / fps * 1000)
33
24
  else:
34
25
  if file_ext == ".webp":
35
- total_duration = 0
36
- frames = 0
37
-
38
- with open(file, "r+b") as f:
39
- mm = mmap.mmap(f.fileno(), 0)
40
- while True:
41
- anmf_pos = mm.find(b"ANMF")
42
- if anmf_pos == -1:
43
- break
44
- mm.seek(anmf_pos + 20)
45
- frame_duration_32 = mm.read(4)
46
- frame_duration = frame_duration_32[:-1] + bytes(
47
- int(frame_duration_32[-1]) & 0b11111100
48
- )
49
- total_duration += int.from_bytes(frame_duration, "little")
50
- frames += 1
51
-
52
- if frames == 0:
53
- fps = 1
54
- else:
55
- fps = frames / total_duration * 1000
26
+ frames, duration = CodecInfo._get_file_frames_duration_webp(file)
56
27
  elif file_ext in (".gif", ".apng", ".png"):
57
- total_duration = 0
58
- frames = len([* iio.imiter(file, plugin="pillow")])
28
+ frames, duration = CodecInfo._get_file_frames_duration_pillow(file)
29
+ else:
30
+ frames, duration = CodecInfo._get_file_frames_duration_av(file)
31
+
32
+ fps = frames / duration * 1000
33
+
34
+ return fps, frames, duration
35
+
36
+ @staticmethod
37
+ def get_file_fps(file: str) -> float:
38
+ file_ext = CodecInfo.get_file_ext(file)
39
+
40
+ if file_ext == ".tgs":
41
+ return CodecInfo._get_file_fps_tgs(file)
42
+ elif file_ext == ".webp":
43
+ frames, duration = CodecInfo._get_file_frames_duration_webp(file)
44
+ elif file_ext in (".gif", ".apng", ".png"):
45
+ frames, duration = CodecInfo._get_file_frames_duration_pillow(file)
46
+ else:
47
+ frames, duration = CodecInfo._get_file_frames_duration_av(file, frames_to_iterate=10)
48
+
49
+ return frames / duration * 1000
50
+
51
+ @staticmethod
52
+ def get_file_frames(file: str, check_anim: bool = False) -> int:
53
+ # If check_anim is True, return value > 1 means the file is animated
54
+ file_ext = CodecInfo.get_file_ext(file)
55
+
56
+ if file_ext == ".tgs":
57
+ return CodecInfo._get_file_frames_tgs(file)
58
+ elif file_ext in (".gif", ".webp", ".png", ".apng"):
59
+ frames, _ = CodecInfo._get_file_frames_duration_pillow(file, frames_only=True)
60
+ else:
61
+ if check_anim == True:
62
+ frames_to_iterate = 2
63
+ else:
64
+ frames_to_iterate = None
65
+ frames, _ = CodecInfo._get_file_frames_duration_av(file, frames_only=True, frames_to_iterate=frames_to_iterate)
66
+
67
+ return frames
68
+
69
+ @staticmethod
70
+ def get_file_duration(file: str) -> int:
71
+ # Return duration in miliseconds
72
+ file_ext = CodecInfo.get_file_ext(file)
73
+
74
+ if file_ext == ".tgs":
75
+ fps, frames = CodecInfo._get_file_fps_frames_tgs(file)
76
+ duration = int(frames / fps * 1000)
77
+ elif file_ext == ".webp":
78
+ _, duration = CodecInfo._get_file_frames_duration_webp(file)
79
+ elif file_ext in (".gif", ".png", ".apng"):
80
+ _, duration = CodecInfo._get_file_frames_duration_pillow(file)
81
+ else:
82
+ _, duration = CodecInfo._get_file_frames_duration_av(file)
83
+
84
+ return duration
85
+
86
+ @staticmethod
87
+ def _get_file_fps_tgs(file: str) -> int:
88
+ from rlottie_python import LottieAnimation # type: ignore
89
+
90
+ with LottieAnimation.from_tgs(file) as anim:
91
+ return anim.lottie_animation_get_framerate()
92
+
93
+ @staticmethod
94
+ def _get_file_frames_tgs(file: str) -> int:
95
+ from rlottie_python import LottieAnimation # type: ignore
96
+
97
+ with LottieAnimation.from_tgs(file) as anim:
98
+ return anim.lottie_animation_get_totalframe()
99
+
100
+ @staticmethod
101
+ def _get_file_fps_frames_tgs(file: str) -> tuple[int, int]:
102
+ from rlottie_python import LottieAnimation # type: ignore
103
+
104
+ with LottieAnimation.from_tgs(file) as anim:
105
+ fps = anim.lottie_animation_get_framerate()
106
+ frames = anim.lottie_animation_get_totalframe()
59
107
 
60
- for frame in range(frames):
61
- metadata = iio.immeta(
62
- file, index=frame, plugin="pillow", exclude_applied=False
108
+ return fps, frames
109
+
110
+ @staticmethod
111
+ def _get_file_frames_duration_pillow(file: str, frames_only: bool = False) -> tuple[int, int]:
112
+ total_duration = 0
113
+
114
+ with Image.open(file) as im:
115
+ if 'n_frames' in im.__dir__():
116
+ frames = im.n_frames
117
+ if frames_only == True:
118
+ return frames, 1
119
+ for i in range(im.n_frames):
120
+ im.seek(i)
121
+ total_duration += im.info.get('duration', 1000)
122
+ return frames, total_duration
123
+ else:
124
+ return 1, 1
125
+
126
+ @staticmethod
127
+ def _get_file_frames_duration_webp(file: str) -> tuple[int, int]:
128
+ total_duration = 0
129
+ frames = 0
130
+
131
+ with open(file, "r+b") as f:
132
+ with mmap.mmap(f.fileno(), 0) as mm:
133
+ while True:
134
+ anmf_pos = mm.find(b"ANMF")
135
+ if anmf_pos == -1:
136
+ break
137
+ mm.seek(anmf_pos + 20)
138
+ frame_duration_32 = mm.read(4)
139
+ frame_duration = frame_duration_32[:-1] + bytes(
140
+ int(frame_duration_32[-1]) & 0b11111100
63
141
  )
64
- total_duration += metadata.get("duration", 1000)
65
-
66
- if frames == 0 or total_duration == 0:
67
- fps = 1
68
- else:
69
- fps = frames / total_duration * 1000
142
+ total_duration += int.from_bytes(frame_duration, "little")
143
+ frames += 1
144
+
145
+ if frames == 0:
146
+ return 1, 1
147
+ else:
148
+ return frames, total_duration
149
+
150
+ @staticmethod
151
+ def _get_file_frames_duration_av(file: str, frames_to_iterate: Optional[int] = None, frames_only: bool = False) -> tuple[int, float]:
152
+ import av # type: ignore
153
+
154
+ # Getting fps from metadata is not reliable
155
+ # Example: https://github.com/laggykiller/sticker-convert/issues/114
156
+
157
+ with av.open(file) as container:
158
+ stream = container.streams.video[0]
159
+
160
+ if frames_only == True and stream.frames > 1:
161
+ return stream.frames, 1
162
+
163
+ last_frame = None
164
+ for frame_count, frame in enumerate(container.decode(stream)):
165
+ last_frame = frame
166
+
167
+ if frames_to_iterate != None and frame_count > frames_to_iterate:
168
+ break
169
+
170
+ if frame_count <= 1:
171
+ return 1, 1
70
172
  else:
71
- # Getting fps from metadata is not reliable
72
- # Example: https://github.com/laggykiller/sticker-convert/issues/114
73
- metadata = iio.immeta(file, plugin='pyav', exclude_applied=False)
74
- context = None
75
- if metadata.get('video_format') == 'yuv420p':
76
- if metadata.get('codec') == 'vp8':
77
- context = CodecContext.create('vp8', 'r')
78
- elif metadata.get('codec') == 'vp9':
79
- context = CodecContext.create('libvpx-vp9', 'r')
80
-
81
- with av.open(file) as container:
82
- stream = container.streams.video[0]
83
- if not context:
84
- context = stream.codec_context
85
-
86
- frames = [i for i in container.decode(stream)]
87
- if len(frames) == 1:
88
- fps = 1
89
- else:
90
- # Need to minus one as dts is the start time of frame
91
- # Last frame dts is not last second of frame / whole video
92
- fps = (len(frames) - 1) / (frames[-1].dts * frames[-1].time_base.numerator / frames[-1].time_base.denominator)
93
-
94
- return fps
173
+ duration = last_frame.pts * last_frame.time_base.numerator / last_frame.time_base.denominator * 1000
174
+ return frame_count, duration
95
175
 
96
176
  @staticmethod
97
177
  def get_file_codec(file: str) -> Optional[str]:
98
178
  codec = None
99
179
  try:
100
- im = Image.open(file)
101
- codec = im.format
180
+ with Image.open(file) as im:
181
+ codec = im.format
182
+ animated = im.is_animated
102
183
  except UnidentifiedImageError:
103
184
  pass
104
-
105
- # Unable to distinguish apng and png
106
- if codec == None:
107
- metadata = iio.immeta(file, plugin="pyav", exclude_applied=False)
108
- codec = metadata.get("codec", None)
109
- if codec == None:
110
- raise RuntimeError(f"Unable to get codec for file {file}")
111
- return codec
112
- elif codec == "PNG":
113
- if im.is_animated:
185
+
186
+ if codec == "PNG":
187
+ # Unable to distinguish apng and png
188
+ if animated:
114
189
  return "apng"
115
190
  else:
116
191
  return "png"
117
- else:
192
+ elif codec != None:
118
193
  return codec.lower()
194
+
195
+ import av # type: ignore
196
+
197
+ with av.open(file) as container:
198
+ codec = container.streams.video[0].codec_context.name
199
+ if codec == None:
200
+ raise RuntimeError(f"Unable to get codec for file {file}")
201
+ return codec.lower()
119
202
 
120
203
  @staticmethod
121
204
  def get_file_res(file: str) -> tuple[int, int]:
122
205
  file_ext = CodecInfo.get_file_ext(file)
123
206
 
124
207
  if file_ext == ".tgs":
125
- with LottieAnimation.from_tgs(file) as anim:
126
- width, height = anim.lottie_animation_get_size()
127
- else:
128
- if file_ext in (".webp", ".png", ".apng"):
129
- plugin = "pillow"
130
- else:
131
- plugin = "pyav"
132
- frame = iio.imread(file, plugin=plugin, index=0)
133
- width = frame.shape[0]
134
- height = frame.shape[1]
135
-
136
- return width, height
208
+ from rlottie_python import LottieAnimation # type: ignore
137
209
 
138
- @staticmethod
139
- def get_file_frames(file: str) -> int:
140
- file_ext = CodecInfo.get_file_ext(file)
141
-
142
- frames = None
143
-
144
- if file_ext == ".tgs":
145
210
  with LottieAnimation.from_tgs(file) as anim:
146
- frames = anim.lottie_animation_get_totalframe()
211
+ width, height = anim.lottie_animation_get_size()
212
+ elif file_ext in (".webp", ".png", ".apng"):
213
+ with Image.open(file) as im:
214
+ width = im.width
215
+ height = im.height
147
216
  else:
148
- if file_ext in (".webp", ".png", ".apng"):
149
- frames = Image.open(file).n_frames
150
- else:
151
- frames = frames = len([* iio.imiter(file, plugin="pyav")])
217
+ import av # type: ignore
152
218
 
153
- return frames
219
+ with av.open(file) as container:
220
+ stream = container.streams.video[0]
221
+ width = stream.width
222
+ height = stream.height
154
223
 
155
- @staticmethod
156
- def get_file_duration(file: str) -> float:
157
- # Return duration in miliseconds
158
- return CodecInfo.get_file_frames(file) / CodecInfo.get_file_fps(file) * 1000
224
+ return width, height
159
225
 
160
226
  @staticmethod
161
227
  def get_file_ext(file: str) -> str:
@@ -163,7 +229,7 @@ class CodecInfo:
163
229
 
164
230
  @staticmethod
165
231
  def is_anim(file: str) -> bool:
166
- if CodecInfo.get_file_frames(file) > 1:
232
+ if CodecInfo.get_file_frames(file, check_anim=True) > 1:
167
233
  return True
168
234
  else:
169
235
  return False
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env python3
2
2
  import os
3
- import unicodedata
4
- import re
5
3
  from typing import Optional, Union
6
4
 
7
5
  from .codec_info import CodecInfo # type: ignore
@@ -11,14 +9,18 @@ from ...job_option import CompOption # type: ignore
11
9
  class FormatVerify:
12
10
  @staticmethod
13
11
  def check_file(file: str, spec: CompOption) -> bool:
12
+ if FormatVerify.check_presence(file) == False:
13
+ return False
14
+
15
+ file_info = CodecInfo(file)
16
+
14
17
  return (
15
- FormatVerify.check_presence(file)
16
- and FormatVerify.check_file_res(file, res=spec.res, square=spec.square)
17
- and FormatVerify.check_file_fps(file, fps=spec.fps)
18
- and FormatVerify.check_file_size(file, size=spec.size_max)
19
- and FormatVerify.check_animated(file, animated=spec.animated)
20
- and FormatVerify.check_format(file, fmt=spec.format)
21
- and FormatVerify.check_duration(file, duration=spec.duration)
18
+ FormatVerify.check_file_res(file, res=spec.res, square=spec.square, file_info=file_info)
19
+ and FormatVerify.check_file_fps(file, fps=spec.fps, file_info=file_info)
20
+ and FormatVerify.check_file_duration(file, duration=spec.duration, file_info=file_info)
21
+ and FormatVerify.check_file_size(file, size=spec.size_max, file_info=file_info)
22
+ and FormatVerify.check_animated(file, animated=spec.animated, file_info=file_info)
23
+ and FormatVerify.check_format(file, fmt=spec.format, file_info=file_info)
22
24
  )
23
25
 
24
26
  @staticmethod
@@ -29,9 +31,14 @@ class FormatVerify:
29
31
  def check_file_res(
30
32
  file: str,
31
33
  res: Optional[list[list[int]]] = None,
32
- square: Optional[bool] = None
34
+ square: Optional[bool] = None,
35
+ file_info: Optional[CodecInfo] = None
33
36
  ) -> bool:
34
- file_width, file_height = CodecInfo.get_file_res(file)
37
+
38
+ if file_info:
39
+ file_width, file_height = file_info.res
40
+ else:
41
+ file_width, file_height = CodecInfo.get_file_res(file)
35
42
 
36
43
  if res:
37
44
  if res[0][0] and file_width < res[0][0]:
@@ -48,8 +55,15 @@ class FormatVerify:
48
55
  return True
49
56
 
50
57
  @staticmethod
51
- def check_file_fps(file: str, fps: Optional[list[int]]) -> bool:
52
- file_fps = CodecInfo.get_file_fps(file)
58
+ def check_file_fps(
59
+ file: str,fps: Optional[list[int]],
60
+ file_info: Optional[CodecInfo] = None
61
+ ) -> bool:
62
+
63
+ if file_info:
64
+ file_fps = file_info.fps
65
+ else:
66
+ file_fps = CodecInfo.get_file_fps(file)
53
67
 
54
68
  if fps and fps[0] and file_fps < fps[0]:
55
69
  return False
@@ -57,11 +71,38 @@ class FormatVerify:
57
71
  return False
58
72
 
59
73
  return True
74
+
75
+ @staticmethod
76
+ def check_file_duration(
77
+ file: str,
78
+ duration: Optional[list[str]] = None,
79
+ file_info: Optional[CodecInfo] = None
80
+ ) -> bool:
81
+
82
+ if file_info:
83
+ file_duration = file_info.duration
84
+ else:
85
+ file_duration = CodecInfo.get_file_duration(file)
86
+
87
+ if duration and duration[0] and file_duration < duration[0]:
88
+ return False
89
+ if duration and duration[1] and file_duration > duration[1]:
90
+ return False
91
+
92
+ return True
60
93
 
61
94
  @staticmethod
62
- def check_file_size(file: str, size: Optional[list[int]] = None) -> bool:
95
+ def check_file_size(
96
+ file: str,
97
+ size: Optional[list[int]] = None,
98
+ file_info: Optional[CodecInfo] = None
99
+ ) -> bool:
100
+
63
101
  file_size = os.path.getsize(file)
64
- file_animated = CodecInfo.is_anim(file)
102
+ if file_info:
103
+ file_animated = file_info.is_animated
104
+ else:
105
+ file_animated = CodecInfo.is_anim(file)
65
106
 
66
107
  if (
67
108
  file_animated == True
@@ -81,8 +122,18 @@ class FormatVerify:
81
122
  return True
82
123
 
83
124
  @staticmethod
84
- def check_animated(file: str, animated: Optional[bool] = None) -> bool:
85
- if animated != None and CodecInfo.is_anim(file) != animated:
125
+ def check_animated(
126
+ file: str,
127
+ animated: Optional[bool] = None,
128
+ file_info: Optional[CodecInfo] = None
129
+ ) -> bool:
130
+
131
+ if file_info:
132
+ file_animated = file_info.is_animated
133
+ else:
134
+ file_animated = CodecInfo.is_anim(file)
135
+
136
+ if animated != None and file_animated != animated:
86
137
  return False
87
138
 
88
139
  return True
@@ -90,8 +141,17 @@ class FormatVerify:
90
141
  @staticmethod
91
142
  def check_format(
92
143
  file: str,
93
- fmt: list[Union[list[str], str, None]] = None
144
+ fmt: list[Union[list[str], str, None]] = None,
145
+ file_info: Optional[CodecInfo] = None
94
146
  ):
147
+
148
+ if file_info:
149
+ file_animated = file_info.is_animated
150
+ file_ext = file_info.file_ext
151
+ else:
152
+ file_animated = CodecInfo.is_anim(file)
153
+ file_ext = CodecInfo.get_file_ext(file)
154
+
95
155
  compat_ext = {
96
156
  ".jpg": ".jpeg",
97
157
  ".jpeg": ".jpg",
@@ -104,7 +164,7 @@ class FormatVerify:
104
164
  if fmt == [None, None]:
105
165
  return True
106
166
 
107
- if FormatVerify.check_animated(file):
167
+ if file_animated:
108
168
  if isinstance(fmt[1], list):
109
169
  formats = fmt[1].copy()
110
170
  else:
@@ -119,66 +179,7 @@ class FormatVerify:
119
179
  if f in compat_ext:
120
180
  formats.append(compat_ext.get(f)) # type: ignore[arg-type]
121
181
 
122
- if CodecInfo.get_file_ext(file) not in formats:
182
+ if file_ext not in formats:
123
183
  return False
124
184
 
125
185
  return True
126
-
127
- @staticmethod
128
- def check_duration(file: str, duration: Optional[list[str]] = None) -> bool:
129
- file_duration = CodecInfo.get_file_duration(file)
130
- if duration and duration[0] and file_duration < duration[0]:
131
- return False
132
- if duration and duration[1] and file_duration > duration[1]:
133
- return False
134
-
135
- return True
136
-
137
- @staticmethod
138
- def sanitize_filename(filename: str) -> str:
139
- # Based on https://gitlab.com/jplusplus/sanitize-filename/-/blob/master/sanitize_filename/sanitize_filename.py
140
- # Replace illegal character with '_'
141
- """Return a fairly safe version of the filename.
142
-
143
- We don't limit ourselves to ascii, because we want to keep municipality
144
- names, etc, but we do want to get rid of anything potentially harmful,
145
- and make sure we do not exceed Windows filename length limits.
146
- Hence a less safe blacklist, rather than a whitelist.
147
- """
148
- blacklist = ["\\", "/", ":", "*", "?", '"', "<", ">", "|", "\0"]
149
- reserved = [
150
- "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5",
151
- "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5",
152
- "LPT6", "LPT7", "LPT8", "LPT9",
153
- ] # Reserved words on Windows
154
- filename = "".join(c if c not in blacklist else "_" for c in filename)
155
- # Remove all charcters below code point 32
156
- filename = "".join(c if 31 < ord(c) else "_" for c in filename)
157
- filename = unicodedata.normalize("NFKD", filename)
158
- filename = filename.rstrip(". ") # Windows does not allow these at end
159
- filename = filename.strip()
160
- if all([x == "." for x in filename]):
161
- filename = "__" + filename
162
- if filename in reserved:
163
- filename = "__" + filename
164
- if len(filename) == 0:
165
- filename = "__"
166
- if len(filename) > 255:
167
- parts = re.split(r"/|\\", filename)[-1].split(".")
168
- if len(parts) > 1:
169
- ext = "." + parts.pop()
170
- filename = filename[: -len(ext)]
171
- else:
172
- ext = ""
173
- if filename == "":
174
- filename = "__"
175
- if len(ext) > 254:
176
- ext = ext[254:]
177
- maxl = 255 - len(ext)
178
- filename = filename[:maxl]
179
- filename = filename + ext
180
- # Re-check last character (if there was no extension)
181
- filename = filename.rstrip(". ")
182
- if len(filename) == 0:
183
- filename = "__"
184
- return filename