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.
- sticker_convert/__init__.py +1 -1
- sticker_convert/cli.py +15 -7
- sticker_convert/converter.py +180 -118
- 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 -119
- sticker_convert/utils/media/format_verify.py +80 -79
- {sticker_convert-2.3.0.dist-info → sticker_convert-2.4.0.dist-info}/METADATA +25 -8
- {sticker_convert-2.3.0.dist-info → sticker_convert-2.4.0.dist-info}/RECORD +22 -21
- {sticker_convert-2.3.0.dist-info → sticker_convert-2.4.0.dist-info}/LICENSE +0 -0
- {sticker_convert-2.3.0.dist-info → sticker_convert-2.4.0.dist-info}/WHEEL +0 -0
- {sticker_convert-2.3.0.dist-info → sticker_convert-2.4.0.dist-info}/entry_points.txt +0 -0
- {sticker_convert-2.3.0.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,161 +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('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
|
-
|
101
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
219
|
+
with av.open(file) as container:
|
220
|
+
stream = container.streams.video[0]
|
221
|
+
width = stream.width
|
222
|
+
height = stream.height
|
154
223
|
|
155
|
-
|
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.
|
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
|