sticker-convert 2.3.1__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.
- sticker_convert/__init__.py +1 -1
- sticker_convert/cli.py +15 -7
- sticker_convert/converter.py +177 -115
- sticker_convert/downloaders/download_line.py +7 -3
- sticker_convert/gui.py +15 -5
- sticker_convert/gui_components/frames/comp_frame.py +5 -0
- sticker_convert/gui_components/windows/advanced_compression_window.py +55 -15
- sticker_convert/job.py +20 -0
- sticker_convert/job_option.py +13 -4
- sticker_convert/resources/compression.json +92 -42
- sticker_convert/resources/help.json +5 -0
- sticker_convert/uploaders/compress_wastickers.py +2 -1
- sticker_convert/uploaders/xcode_imessage.py +2 -1
- sticker_convert/utils/files/sanitize_filename.py +51 -0
- sticker_convert/utils/media/codec_info.py +185 -122
- sticker_convert/utils/media/format_verify.py +80 -79
- {sticker_convert-2.3.1.dist-info → sticker_convert-2.4.0.dist-info}/METADATA +25 -8
- {sticker_convert-2.3.1.dist-info → sticker_convert-2.4.0.dist-info}/RECORD +22 -21
- {sticker_convert-2.3.1.dist-info → sticker_convert-2.4.0.dist-info}/LICENSE +0 -0
- {sticker_convert-2.3.1.dist-info → sticker_convert-2.4.0.dist-info}/WHEEL +0 -0
- {sticker_convert-2.3.1.dist-info → sticker_convert-2.4.0.dist-info}/entry_points.txt +0 -0
- {sticker_convert-2.3.1.dist-info → sticker_convert-2.4.0.dist-info}/top_level.txt +0 -0
@@ -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,164 +1,227 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
from __future__ import annotations
|
3
3
|
import os
|
4
|
-
import
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
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
|
31
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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 +=
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
72
|
-
|
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('libvpx', '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
|
-
last_frame = None
|
87
|
-
for frame_count, frame in enumerate(container.decode(stream)):
|
88
|
-
last_frame = frame
|
89
|
-
if frame_count > 10:
|
90
|
-
break
|
91
|
-
|
92
|
-
if frame_count <= 1:
|
93
|
-
fps = 1
|
94
|
-
else:
|
95
|
-
fps = frame_count / (last_frame.pts * last_frame.time_base.numerator / last_frame.time_base.denominator)
|
96
|
-
|
97
|
-
return fps
|
173
|
+
duration = last_frame.pts * last_frame.time_base.numerator / last_frame.time_base.denominator * 1000
|
174
|
+
return frame_count, duration
|
98
175
|
|
99
176
|
@staticmethod
|
100
177
|
def get_file_codec(file: str) -> Optional[str]:
|
101
178
|
codec = None
|
102
179
|
try:
|
103
|
-
|
104
|
-
|
180
|
+
with Image.open(file) as im:
|
181
|
+
codec = im.format
|
182
|
+
animated = im.is_animated
|
105
183
|
except UnidentifiedImageError:
|
106
184
|
pass
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
codec = metadata.get("codec", None)
|
112
|
-
if codec == None:
|
113
|
-
raise RuntimeError(f"Unable to get codec for file {file}")
|
114
|
-
return codec
|
115
|
-
elif codec == "PNG":
|
116
|
-
if im.is_animated:
|
185
|
+
|
186
|
+
if codec == "PNG":
|
187
|
+
# Unable to distinguish apng and png
|
188
|
+
if animated:
|
117
189
|
return "apng"
|
118
190
|
else:
|
119
191
|
return "png"
|
120
|
-
|
192
|
+
elif codec != None:
|
121
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()
|
122
202
|
|
123
203
|
@staticmethod
|
124
204
|
def get_file_res(file: str) -> tuple[int, int]:
|
125
205
|
file_ext = CodecInfo.get_file_ext(file)
|
126
206
|
|
127
207
|
if file_ext == ".tgs":
|
128
|
-
|
129
|
-
width, height = anim.lottie_animation_get_size()
|
130
|
-
else:
|
131
|
-
if file_ext in (".webp", ".png", ".apng"):
|
132
|
-
plugin = "pillow"
|
133
|
-
else:
|
134
|
-
plugin = "pyav"
|
135
|
-
frame = iio.imread(file, plugin=plugin, index=0)
|
136
|
-
width = frame.shape[0]
|
137
|
-
height = frame.shape[1]
|
138
|
-
|
139
|
-
return width, height
|
208
|
+
from rlottie_python import LottieAnimation # type: ignore
|
140
209
|
|
141
|
-
@staticmethod
|
142
|
-
def get_file_frames(file: str) -> int:
|
143
|
-
file_ext = CodecInfo.get_file_ext(file)
|
144
|
-
|
145
|
-
frames = None
|
146
|
-
|
147
|
-
if file_ext == ".tgs":
|
148
210
|
with LottieAnimation.from_tgs(file) as anim:
|
149
|
-
|
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
|
150
216
|
else:
|
151
|
-
|
152
|
-
frames = Image.open(file).n_frames
|
153
|
-
else:
|
154
|
-
frames = frames = len([* iio.imiter(file, plugin="pyav")])
|
217
|
+
import av # type: ignore
|
155
218
|
|
156
|
-
|
219
|
+
with av.open(file) as container:
|
220
|
+
stream = container.streams.video[0]
|
221
|
+
width = stream.width
|
222
|
+
height = stream.height
|
157
223
|
|
158
|
-
|
159
|
-
def get_file_duration(file: str) -> float:
|
160
|
-
# Return duration in miliseconds
|
161
|
-
return CodecInfo.get_file_frames(file) / CodecInfo.get_file_fps(file) * 1000
|
224
|
+
return width, height
|
162
225
|
|
163
226
|
@staticmethod
|
164
227
|
def get_file_ext(file: str) -> str:
|
@@ -166,7 +229,7 @@ class CodecInfo:
|
|
166
229
|
|
167
230
|
@staticmethod
|
168
231
|
def is_anim(file: str) -> bool:
|
169
|
-
if CodecInfo.get_file_frames(file) > 1:
|
232
|
+
if CodecInfo.get_file_frames(file, check_anim=True) > 1:
|
170
233
|
return True
|
171
234
|
else:
|
172
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.
|
16
|
-
and FormatVerify.
|
17
|
-
and FormatVerify.
|
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
|
-
|
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(
|
52
|
-
|
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(
|
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
|
-
|
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(
|
85
|
-
|
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
|
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
|
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
|