sticker-convert 2.7.12__py3-none-any.whl → 2.8.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/converter.py +98 -52
- sticker_convert/utils/media/codec_info.py +117 -37
- sticker_convert/version.py +1 -1
- {sticker_convert-2.7.12.dist-info → sticker_convert-2.8.0.dist-info}/METADATA +1 -1
- {sticker_convert-2.7.12.dist-info → sticker_convert-2.8.0.dist-info}/RECORD +9 -9
- {sticker_convert-2.7.12.dist-info → sticker_convert-2.8.0.dist-info}/LICENSE +0 -0
- {sticker_convert-2.7.12.dist-info → sticker_convert-2.8.0.dist-info}/WHEEL +0 -0
- {sticker_convert-2.7.12.dist-info → sticker_convert-2.8.0.dist-info}/entry_points.txt +0 -0
- {sticker_convert-2.7.12.dist-info → sticker_convert-2.8.0.dist-info}/top_level.txt +0 -0
sticker_convert/converter.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
import os
|
3
|
-
from decimal import ROUND_HALF_UP, Decimal
|
4
3
|
from fractions import Fraction
|
5
4
|
from io import BytesIO
|
6
5
|
from math import ceil, floor
|
@@ -15,12 +14,12 @@ from PIL import __version__ as PillowVersion
|
|
15
14
|
from sticker_convert.job_option import CompOption
|
16
15
|
from sticker_convert.utils.callback import Callback, CallbackReturn, CbQueueItemType
|
17
16
|
from sticker_convert.utils.files.cache_store import CacheStore
|
18
|
-
from sticker_convert.utils.media.codec_info import CodecInfo
|
17
|
+
from sticker_convert.utils.media.codec_info import CodecInfo, rounding
|
19
18
|
from sticker_convert.utils.media.format_verify import FormatVerify
|
20
19
|
|
21
20
|
if TYPE_CHECKING:
|
22
|
-
from av.video.frame import VideoFrame
|
23
|
-
from av.video.plane import VideoPlane
|
21
|
+
from av.video.frame import VideoFrame
|
22
|
+
from av.video.plane import VideoPlane
|
24
23
|
|
25
24
|
MSG_START_COMP = "[I] Start compressing {} -> {}"
|
26
25
|
MSG_SKIP_COMP = "[S] Compatible file found, skip compress and just copy {} -> {}"
|
@@ -44,10 +43,6 @@ YUV_RGB_MATRIX = np.array(
|
|
44
43
|
)
|
45
44
|
|
46
45
|
|
47
|
-
def rounding(value: float) -> Decimal:
|
48
|
-
return Decimal(value).quantize(0, ROUND_HALF_UP)
|
49
|
-
|
50
|
-
|
51
46
|
def get_step_value(
|
52
47
|
max_step: Optional[int],
|
53
48
|
min_step: Optional[int],
|
@@ -264,14 +259,21 @@ class StickerConvert:
|
|
264
259
|
self.result_size = self.size
|
265
260
|
self.result_step = step_current
|
266
261
|
|
267
|
-
if
|
262
|
+
if (
|
263
|
+
step_upper - step_lower > 0
|
264
|
+
and step_current != step_lower
|
265
|
+
and self.size_max
|
266
|
+
):
|
268
267
|
if self.size <= self.size_max:
|
269
268
|
sign = "<"
|
270
269
|
step_upper = step_current
|
271
270
|
else:
|
272
271
|
sign = ">"
|
273
272
|
step_lower = step_current
|
274
|
-
step_current
|
273
|
+
if step_current == step_lower + 1:
|
274
|
+
step_current = step_lower
|
275
|
+
else:
|
276
|
+
step_current = int(rounding((step_lower + step_upper) / 2))
|
275
277
|
self.recompress(sign)
|
276
278
|
elif self.result:
|
277
279
|
return self.compress_done(self.result, self.result_step)
|
@@ -409,7 +411,7 @@ class StickerConvert:
|
|
409
411
|
|
410
412
|
if suffix in (".tgs", ".lottie", ".json"):
|
411
413
|
self._frames_import_lottie()
|
412
|
-
elif suffix in (".webp", ".apng", "png"):
|
414
|
+
elif suffix in (".webp", ".apng", ".png", ".gif"):
|
413
415
|
# ffmpeg do not support webp decoding (yet)
|
414
416
|
# ffmpeg could fail to decode apng if file is buggy
|
415
417
|
self._frames_import_pillow()
|
@@ -419,10 +421,24 @@ class StickerConvert:
|
|
419
421
|
def _frames_import_pillow(self) -> None:
|
420
422
|
with Image.open(self.in_f) as im:
|
421
423
|
# Note: im.convert("RGBA") would return rgba image of current frame only
|
422
|
-
if
|
423
|
-
|
424
|
-
|
424
|
+
if (
|
425
|
+
"n_frames" in dir(im)
|
426
|
+
and im.n_frames != 0
|
427
|
+
and self.codec_info_orig.fps != 0.0
|
428
|
+
):
|
429
|
+
duration_ptr = 0.0
|
430
|
+
duration_inc = 1 / self.codec_info_orig.fps * 1000
|
431
|
+
next_frame_start_duration = im.info.get("duration", 1000)
|
432
|
+
frame = 0
|
433
|
+
while True:
|
425
434
|
self.frames_raw.append(np.asarray(im.convert("RGBA")))
|
435
|
+
duration_ptr += duration_inc
|
436
|
+
if duration_ptr >= next_frame_start_duration:
|
437
|
+
if frame == im.n_frames:
|
438
|
+
break
|
439
|
+
im.seek(frame)
|
440
|
+
next_frame_start_duration += im.info.get("duration", 1000)
|
441
|
+
frame += 1
|
426
442
|
else:
|
427
443
|
self.frames_raw.append(np.asarray(im.convert("RGBA")))
|
428
444
|
|
@@ -431,6 +447,7 @@ class StickerConvert:
|
|
431
447
|
from av.codec.context import CodecContext
|
432
448
|
from av.container.input import InputContainer
|
433
449
|
from av.video.codeccontext import VideoCodecContext
|
450
|
+
from av.video.frame import VideoFrame
|
434
451
|
|
435
452
|
# Crashes when handling some webm in yuv420p and convert to rgba
|
436
453
|
# https://github.com/PyAV-Org/PyAV/issues/1166
|
@@ -451,19 +468,33 @@ class StickerConvert:
|
|
451
468
|
|
452
469
|
for packet in container.demux(container.streams.video):
|
453
470
|
for frame in context.decode(packet):
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
471
|
+
width_orig = frame.width
|
472
|
+
height_orig = frame.height
|
473
|
+
|
474
|
+
# Need to pad frame to even dimension first
|
475
|
+
if width_orig % 2 == 1 or height_orig % 2 == 1:
|
476
|
+
from av.filter import Graph
|
477
|
+
|
478
|
+
width_new = width_orig + width_orig % 2
|
479
|
+
height_new = height_orig + height_orig % 2
|
480
|
+
|
481
|
+
graph = Graph()
|
482
|
+
in_src = graph.add_buffer(template=container.streams.video[0])
|
483
|
+
pad = graph.add(
|
484
|
+
"pad", f"{width_new}:{height_new}:0:0:color=#00000000"
|
485
|
+
)
|
486
|
+
in_src.link_to(pad)
|
487
|
+
sink = graph.add("buffersink")
|
488
|
+
pad.link_to(sink)
|
489
|
+
graph.configure()
|
458
490
|
|
459
|
-
|
460
|
-
|
491
|
+
graph.push(frame)
|
492
|
+
frame_resized = cast(VideoFrame, graph.pull())
|
461
493
|
else:
|
462
|
-
|
494
|
+
frame_resized = frame
|
463
495
|
|
464
|
-
if
|
465
|
-
rgb_array =
|
466
|
-
cast("np.ndarray[Any, Any]", rgb_array)
|
496
|
+
if frame_resized.format.name == "yuv420p":
|
497
|
+
rgb_array = frame_resized.to_ndarray(format="rgb24")
|
467
498
|
rgba_array = np.dstack(
|
468
499
|
(
|
469
500
|
rgb_array,
|
@@ -474,14 +505,14 @@ class StickerConvert:
|
|
474
505
|
# yuva420p may cause crash
|
475
506
|
# Not safe to directly call frame.to_ndarray(format="rgba")
|
476
507
|
# https://github.com/laggykiller/sticker-convert/issues/114
|
477
|
-
|
478
|
-
width=width,
|
479
|
-
height=height,
|
508
|
+
frame_resized = frame_resized.reformat(
|
480
509
|
format="yuva420p",
|
481
510
|
dst_colorspace=1,
|
482
511
|
)
|
483
|
-
rgba_array = yuva_to_rgba(
|
512
|
+
rgba_array = yuva_to_rgba(frame_resized)
|
484
513
|
|
514
|
+
# Remove pixels that was added to make dimensions even
|
515
|
+
rgba_array = rgba_array[0:width_orig, 0:height_orig]
|
485
516
|
self.frames_raw.append(rgba_array)
|
486
517
|
|
487
518
|
def _frames_import_lottie(self) -> None:
|
@@ -514,25 +545,25 @@ class StickerConvert:
|
|
514
545
|
anim.lottie_animation_destroy()
|
515
546
|
|
516
547
|
def determine_bg_color(self) -> Tuple[int, int, int, int]:
|
548
|
+
mean_total = 0.0
|
517
549
|
# Calculate average color of all frames for selecting background color
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
550
|
+
for frame in self.frames_raw:
|
551
|
+
s = frame.shape
|
552
|
+
colors = frame.reshape((-1, s[2])) # type: ignore
|
553
|
+
# Do not count in alpha=0
|
554
|
+
# If alpha > 0, use alpha as weight
|
555
|
+
colors = colors[colors[:, 3] != 0]
|
556
|
+
if colors.shape[0] != 0:
|
557
|
+
alphas = colors[:, 3] / 255
|
558
|
+
r_mean = np.mean(colors[:, 0] * alphas)
|
559
|
+
g_mean = np.mean(colors[:, 1] * alphas)
|
560
|
+
b_mean = np.mean(colors[:, 2] * alphas)
|
561
|
+
mean_total += (r_mean + g_mean + b_mean) / 3
|
562
|
+
|
563
|
+
if mean_total / len(self.frames_raw) < 128:
|
564
|
+
return (255, 255, 255, 0)
|
526
565
|
else:
|
527
|
-
|
528
|
-
r_mean = np.mean(colors[:, 0] * alphas)
|
529
|
-
g_mean = np.mean(colors[:, 1] * alphas)
|
530
|
-
b_mean = np.mean(colors[:, 2] * alphas)
|
531
|
-
mean = (r_mean + g_mean + b_mean) / 3
|
532
|
-
if mean < 128:
|
533
|
-
return (255, 255, 255, 0)
|
534
|
-
else:
|
535
|
-
return (0, 0, 0, 0)
|
566
|
+
return (0, 0, 0, 0)
|
536
567
|
|
537
568
|
def frames_resize(
|
538
569
|
self, frames_in: "List[np.ndarray[Any, Any]]"
|
@@ -627,8 +658,6 @@ class StickerConvert:
|
|
627
658
|
frame_current = 0
|
628
659
|
frame_current_float = 0.0
|
629
660
|
while True:
|
630
|
-
frame_current_float += frame_increment
|
631
|
-
frame_current = int(rounding(frame_current_float))
|
632
661
|
if frame_current <= len(frames_in) - 1 and not (
|
633
662
|
frames_out_max and len(frames_out) == frames_out_max
|
634
663
|
):
|
@@ -639,6 +668,8 @@ class StickerConvert:
|
|
639
668
|
):
|
640
669
|
frames_out.append(frames_in[-1])
|
641
670
|
return frames_out
|
671
|
+
frame_current_float += frame_increment
|
672
|
+
frame_current = int(rounding(frame_current_float))
|
642
673
|
|
643
674
|
def frames_export(self) -> None:
|
644
675
|
is_animated = len(self.frames_processed) > 1 and self.fps
|
@@ -727,10 +758,10 @@ class StickerConvert:
|
|
727
758
|
if 0 in alpha:
|
728
759
|
extra_kwargs["transparency"] = 0
|
729
760
|
extra_kwargs["disposal"] = 2
|
730
|
-
im_out = [self.quantize(Image.fromarray(i)) for i in frames_processed]
|
761
|
+
im_out = [self.quantize(Image.fromarray(i)) for i in frames_processed] # type: ignore
|
731
762
|
else:
|
732
763
|
im_out = [
|
733
|
-
self.quantize(Image.fromarray(i).convert("RGB")).convert("RGB")
|
764
|
+
self.quantize(Image.fromarray(i).convert("RGB")).convert("RGB") # type: ignore
|
734
765
|
for i in frames_processed
|
735
766
|
]
|
736
767
|
|
@@ -755,10 +786,25 @@ class StickerConvert:
|
|
755
786
|
config = webp.WebPConfig.new(quality=self.quality) # type: ignore
|
756
787
|
enc = webp.WebPAnimEncoder.new(self.res_w, self.res_h) # type: ignore
|
757
788
|
timestamp_ms = 0
|
758
|
-
|
759
|
-
|
789
|
+
timestamp_inc = int(1000 / self.fps)
|
790
|
+
|
791
|
+
pic = webp.WebPPicture.from_numpy(self.frames_processed[0]) # type: ignore
|
792
|
+
enc.encode_frame(pic, 0, config=config) # type: ignore
|
793
|
+
|
794
|
+
frame_num = 1
|
795
|
+
frame_num_prev = 1
|
796
|
+
frame_total = len(self.frames_processed)
|
797
|
+
while frame_num < frame_total - 1:
|
798
|
+
while frame_num < frame_total - 1 and np.array_equal(
|
799
|
+
self.frames_processed[frame_num_prev],
|
800
|
+
self.frames_processed[frame_num],
|
801
|
+
):
|
802
|
+
timestamp_ms += timestamp_inc
|
803
|
+
frame_num += 1
|
804
|
+
pic = webp.WebPPicture.from_numpy(self.frames_processed[frame_num]) # type: ignore
|
760
805
|
enc.encode_frame(pic, timestamp_ms, config=config) # type: ignore
|
761
|
-
|
806
|
+
frame_num_prev = frame_num
|
807
|
+
|
762
808
|
anim_data = enc.assemble(timestamp_ms) # type: ignore
|
763
809
|
self.tmp_f.write(anim_data.buffer()) # type: ignore
|
764
810
|
|
@@ -3,13 +3,64 @@ from __future__ import annotations
|
|
3
3
|
|
4
4
|
import mmap
|
5
5
|
from decimal import ROUND_HALF_UP, Decimal
|
6
|
+
from fractions import Fraction
|
6
7
|
from io import BytesIO
|
8
|
+
from math import gcd
|
7
9
|
from pathlib import Path
|
8
|
-
from typing import BinaryIO, Optional, Tuple, Union, cast
|
10
|
+
from typing import BinaryIO, List, Optional, Tuple, Union, cast
|
9
11
|
|
10
12
|
from PIL import Image, UnidentifiedImageError
|
11
13
|
|
12
14
|
|
15
|
+
def lcm(a: int, b: int):
|
16
|
+
return abs(a * b) // gcd(a, b)
|
17
|
+
|
18
|
+
|
19
|
+
def rounding(value: float) -> Decimal:
|
20
|
+
return Decimal(value).quantize(0, ROUND_HALF_UP)
|
21
|
+
|
22
|
+
|
23
|
+
def fraction_gcd(x: Fraction, y: Fraction) -> Fraction:
|
24
|
+
a = x.numerator
|
25
|
+
b = x.denominator
|
26
|
+
c = y.numerator
|
27
|
+
d = y.denominator
|
28
|
+
return Fraction(gcd(a, c), lcm(b, d))
|
29
|
+
|
30
|
+
|
31
|
+
def fractions_gcd(*fractions: Fraction) -> Fraction:
|
32
|
+
fractions_list = list(fractions)
|
33
|
+
gcd = fractions_list.pop(0)
|
34
|
+
for fraction in fractions_list:
|
35
|
+
gcd = fraction_gcd(gcd, fraction)
|
36
|
+
|
37
|
+
return gcd
|
38
|
+
|
39
|
+
|
40
|
+
def get_five_dec_place(value: float) -> str:
|
41
|
+
return str(value).split(".")[1][:5].ljust(5, "0")
|
42
|
+
|
43
|
+
|
44
|
+
def likely_int(value: float) -> bool:
|
45
|
+
if isinstance(value, int):
|
46
|
+
return True
|
47
|
+
return True if get_five_dec_place(value) in ("99999", "00000") else False
|
48
|
+
|
49
|
+
|
50
|
+
def durations_gcd(*durations: Union[int, float]) -> Union[float, Fraction]:
|
51
|
+
if any(i for i in durations if isinstance(i, float)):
|
52
|
+
if all(i for i in durations if likely_int(i)):
|
53
|
+
return Fraction(gcd(*(int(rounding(i)) for i in durations)), 1)
|
54
|
+
# Test for denominators that can produce recurring decimal
|
55
|
+
for x in (3, 6, 7, 9, 11, 13):
|
56
|
+
if all(likely_int(i * x) for i in durations):
|
57
|
+
return Fraction(gcd(*(int(rounding(i * x)) for i in durations)), x)
|
58
|
+
else:
|
59
|
+
return min(durations)
|
60
|
+
else:
|
61
|
+
return Fraction(gcd(*durations), 1) # type: ignore
|
62
|
+
|
63
|
+
|
13
64
|
class CodecInfo:
|
14
65
|
def __init__(
|
15
66
|
self, file: Union[Path, bytes], file_ext: Optional[str] = None
|
@@ -29,8 +80,9 @@ class CodecInfo:
|
|
29
80
|
@staticmethod
|
30
81
|
def get_file_fps_frames_duration(
|
31
82
|
file: Union[Path, bytes], file_ext: Optional[str] = None
|
32
|
-
) -> Tuple[float, int,
|
83
|
+
) -> Tuple[float, int, float]:
|
33
84
|
fps: float
|
85
|
+
duration: float
|
34
86
|
|
35
87
|
if not file_ext and isinstance(file, Path):
|
36
88
|
file_ext = CodecInfo.get_file_ext(file)
|
@@ -41,13 +93,12 @@ class CodecInfo:
|
|
41
93
|
duration = int(frames / fps * 1000)
|
42
94
|
else:
|
43
95
|
duration = 0
|
96
|
+
elif file_ext == ".webp":
|
97
|
+
fps, frames, duration = CodecInfo._get_file_fps_frames_duration_webp(file)
|
98
|
+
elif file_ext in (".gif", ".apng", ".png"):
|
99
|
+
fps, frames, duration = CodecInfo._get_file_fps_frames_duration_pillow(file)
|
44
100
|
else:
|
45
|
-
|
46
|
-
frames, duration = CodecInfo._get_file_frames_duration_webp(file)
|
47
|
-
elif file_ext in (".gif", ".apng", ".png"):
|
48
|
-
frames, duration = CodecInfo._get_file_frames_duration_pillow(file)
|
49
|
-
else:
|
50
|
-
frames, duration = CodecInfo._get_file_frames_duration_av(file)
|
101
|
+
frames, duration = CodecInfo._get_file_frames_duration_av(file)
|
51
102
|
|
52
103
|
if duration > 0:
|
53
104
|
fps = frames / duration * 1000
|
@@ -63,14 +114,16 @@ class CodecInfo:
|
|
63
114
|
|
64
115
|
if file_ext == ".tgs":
|
65
116
|
return CodecInfo._get_file_fps_tgs(file)
|
66
|
-
|
67
|
-
|
117
|
+
elif file_ext == ".webp":
|
118
|
+
fps, _, _ = CodecInfo._get_file_fps_frames_duration_webp(file)
|
119
|
+
return fps
|
68
120
|
elif file_ext in (".gif", ".apng", ".png"):
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
121
|
+
fps, _, _ = CodecInfo._get_file_fps_frames_duration_pillow(file)
|
122
|
+
return fps
|
123
|
+
|
124
|
+
frames, duration = CodecInfo._get_file_frames_duration_av(
|
125
|
+
file, frames_to_iterate=10
|
126
|
+
)
|
74
127
|
|
75
128
|
if duration > 0:
|
76
129
|
return frames / duration * 1000
|
@@ -89,7 +142,7 @@ class CodecInfo:
|
|
89
142
|
if file_ext == ".tgs":
|
90
143
|
return CodecInfo._get_file_frames_tgs(file)
|
91
144
|
if file_ext in (".gif", ".webp", ".png", ".apng"):
|
92
|
-
frames, _ = CodecInfo.
|
145
|
+
_, frames, _ = CodecInfo._get_file_fps_frames_duration_pillow(
|
93
146
|
file, frames_only=True
|
94
147
|
)
|
95
148
|
else:
|
@@ -106,7 +159,9 @@ class CodecInfo:
|
|
106
159
|
@staticmethod
|
107
160
|
def get_file_duration(
|
108
161
|
file: Union[Path, bytes], file_ext: Optional[str] = None
|
109
|
-
) ->
|
162
|
+
) -> float:
|
163
|
+
duration: float
|
164
|
+
|
110
165
|
# Return duration in miliseconds
|
111
166
|
if not file_ext and isinstance(file, Path):
|
112
167
|
file_ext = CodecInfo.get_file_ext(file)
|
@@ -118,9 +173,9 @@ class CodecInfo:
|
|
118
173
|
else:
|
119
174
|
duration = 0
|
120
175
|
elif file_ext == ".webp":
|
121
|
-
_, duration = CodecInfo.
|
176
|
+
_, _, duration = CodecInfo._get_file_fps_frames_duration_webp(file)
|
122
177
|
elif file_ext in (".gif", ".png", ".apng"):
|
123
|
-
_, duration = CodecInfo.
|
178
|
+
_, _, duration = CodecInfo._get_file_fps_frames_duration_pillow(file)
|
124
179
|
else:
|
125
180
|
_, duration = CodecInfo._get_file_frames_duration_av(file)
|
126
181
|
|
@@ -176,29 +231,46 @@ class CodecInfo:
|
|
176
231
|
return fps, frames
|
177
232
|
|
178
233
|
@staticmethod
|
179
|
-
def
|
234
|
+
def _get_file_fps_frames_duration_pillow(
|
180
235
|
file: Union[Path, bytes], frames_only: bool = False
|
181
|
-
) -> Tuple[int,
|
182
|
-
total_duration = 0
|
236
|
+
) -> Tuple[float, int, float]:
|
237
|
+
total_duration = 0.0
|
238
|
+
durations: List[float] = []
|
183
239
|
|
184
240
|
with Image.open(file) as im:
|
185
241
|
if "n_frames" in dir(im):
|
186
242
|
frames = im.n_frames
|
187
243
|
if frames_only is True:
|
188
|
-
return frames, 1
|
244
|
+
return 0.0, frames, 1
|
189
245
|
for i in range(im.n_frames):
|
190
246
|
im.seek(i)
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
247
|
+
frame_duration = cast(float, im.info.get("duration", 1000))
|
248
|
+
if frame_duration not in durations and frame_duration != 0:
|
249
|
+
durations.append(frame_duration)
|
250
|
+
total_duration += frame_duration
|
251
|
+
if im.n_frames == 0 or total_duration == 0:
|
252
|
+
fps = 0.0
|
253
|
+
elif len(durations) == 1:
|
254
|
+
fps = frames / total_duration * 1000
|
255
|
+
else:
|
256
|
+
duration_gcd = durations_gcd(*durations)
|
257
|
+
frames_apparent = total_duration / duration_gcd
|
258
|
+
fps = frames_apparent / total_duration * 1000
|
259
|
+
return fps, frames, total_duration
|
260
|
+
|
261
|
+
return (
|
262
|
+
0.0,
|
263
|
+
1,
|
264
|
+
0,
|
265
|
+
)
|
195
266
|
|
196
267
|
@staticmethod
|
197
|
-
def
|
268
|
+
def _get_file_fps_frames_duration_webp(
|
198
269
|
file: Union[Path, bytes],
|
199
|
-
) -> Tuple[int, int]:
|
270
|
+
) -> Tuple[float, int, int]:
|
200
271
|
total_duration = 0
|
201
272
|
frames = 0
|
273
|
+
durations: List[int] = []
|
202
274
|
|
203
275
|
def _open_f(file: Union[Path, bytes]) -> BinaryIO:
|
204
276
|
if isinstance(file, Path):
|
@@ -213,16 +285,26 @@ class CodecInfo:
|
|
213
285
|
break
|
214
286
|
mm.seek(anmf_pos + 20)
|
215
287
|
frame_duration_32 = mm.read(4)
|
216
|
-
|
288
|
+
frame_duration_bytes = frame_duration_32[:-1] + bytes(
|
217
289
|
int(frame_duration_32[-1]) & 0b11111100
|
218
290
|
)
|
219
|
-
|
291
|
+
frame_duration = int.from_bytes(frame_duration_bytes, "little")
|
292
|
+
if frame_duration not in durations and frame_duration != 0:
|
293
|
+
durations.append(frame_duration)
|
294
|
+
total_duration += frame_duration
|
220
295
|
frames += 1
|
221
296
|
|
222
297
|
if frames == 0:
|
223
|
-
return
|
298
|
+
return 0.0, 0, 0
|
299
|
+
|
300
|
+
if len(durations) == 1:
|
301
|
+
fps = frames / total_duration * 1000
|
302
|
+
else:
|
303
|
+
duration_gcd = durations_gcd(*durations)
|
304
|
+
frames_apparent = total_duration / duration_gcd
|
305
|
+
fps = float(frames_apparent / total_duration * 1000)
|
224
306
|
|
225
|
-
return frames, total_duration
|
307
|
+
return fps, frames, total_duration
|
226
308
|
|
227
309
|
@staticmethod
|
228
310
|
def _get_file_frames_duration_av(
|
@@ -246,9 +328,7 @@ class CodecInfo:
|
|
246
328
|
container = cast(InputContainer, container)
|
247
329
|
stream = container.streams.video[0]
|
248
330
|
if container.duration:
|
249
|
-
duration_metadata = int(
|
250
|
-
Decimal(container.duration / 1000).quantize(0, ROUND_HALF_UP)
|
251
|
-
)
|
331
|
+
duration_metadata = int(rounding(container.duration / 1000))
|
252
332
|
else:
|
253
333
|
duration_metadata = 0
|
254
334
|
|
@@ -273,7 +353,7 @@ class CodecInfo:
|
|
273
353
|
duration_n_minus_one = last_frame.pts * time_base_ms
|
274
354
|
ms_per_frame = duration_n_minus_one / (frame_count - 1)
|
275
355
|
duration = frame_count * ms_per_frame
|
276
|
-
return frame_count, int(
|
356
|
+
return frame_count, int(rounding(duration))
|
277
357
|
|
278
358
|
return 0, 0
|
279
359
|
|
sticker_convert/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sticker-convert
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.8.0
|
4
4
|
Summary: Convert (animated) stickers to/from WhatsApp, Telegram, Signal, Line, Kakao, iMessage. Written in Python.
|
5
5
|
Author-email: laggykiller <chaudominic2@gmail.com>
|
6
6
|
Maintainer-email: laggykiller <chaudominic2@gmail.com>
|
@@ -1,12 +1,12 @@
|
|
1
1
|
sticker_convert/__init__.py,sha256=iQnv6UOOA69c3soAn7ZOnAIubTIQSUxtq1Uhh8xRWvU,102
|
2
2
|
sticker_convert/__main__.py,sha256=6RJauR-SCSSTT3TU7FFB6B6PVwsCxO2xZXtmZ3jc2Is,463
|
3
3
|
sticker_convert/cli.py,sha256=kyfhvMoHq42uLZsYYLrr6b30xsNE93GVZlbGYPFjC2I,18385
|
4
|
-
sticker_convert/converter.py,sha256
|
4
|
+
sticker_convert/converter.py,sha256=Y0vVZhAxlTe_SDTbQq2_Z9vM8oY6VAJPToYji-S6J-E,35033
|
5
5
|
sticker_convert/definitions.py,sha256=ZhP2ALCEud-w9ZZD4c3TDG9eHGPZyaAL7zPUsJAbjtE,2073
|
6
6
|
sticker_convert/gui.py,sha256=TqdgFbHBRYgcXWWrsfxLUJ8Zu9WeE11vYyZMX6nalik,30599
|
7
7
|
sticker_convert/job.py,sha256=J4e7dZ48t5EhM3fG-xF1BEQ10WYljx3wWamKXAJjCa8,26000
|
8
8
|
sticker_convert/job_option.py,sha256=1YVhyTfu2cWz3qpAKbdIM11jbL0CJz0ksOYAeg8v6dc,7649
|
9
|
-
sticker_convert/version.py,sha256=
|
9
|
+
sticker_convert/version.py,sha256=M_a1EApD3NUMpL5LvuHWnryb8nvQLMKOHwpDKPiWPX4,46
|
10
10
|
sticker_convert/downloaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
11
|
sticker_convert/downloaders/download_base.py,sha256=5R7c8kwahAflOOYrtSQPnBVQ4T-DsprPUMP7G9wcJX4,2824
|
12
12
|
sticker_convert/downloaders/download_kakao.py,sha256=RYrebTxEjKjXAr9xw18r0dMW0dFjSqZxzi8dpaq56TY,8581
|
@@ -89,12 +89,12 @@ sticker_convert/utils/files/metadata_handler.py,sha256=TJpQ-7KdnqQh09hwR6xB_scRL
|
|
89
89
|
sticker_convert/utils/files/run_bin.py,sha256=QalA9je6liHxiOtxsjsFsIkc2t59quhcJCVpP1X3p50,1743
|
90
90
|
sticker_convert/utils/files/sanitize_filename.py,sha256=HBklPGsHRJjFQUIC5rYTQsUrsuTtezZXIEA8CPhLP8A,2156
|
91
91
|
sticker_convert/utils/media/apple_png_normalize.py,sha256=LbrQhc7LlYX4I9ek4XJsZE4l0MygBA1jB-PFiYLEkzk,3657
|
92
|
-
sticker_convert/utils/media/codec_info.py,sha256=
|
92
|
+
sticker_convert/utils/media/codec_info.py,sha256=yXjgGN3AwwFiwPHTlM9mqEtH3fH8VegZpKkgj3TvUNA,15611
|
93
93
|
sticker_convert/utils/media/decrypt_kakao.py,sha256=4wq9ZDRnFkx1WmFZnyEogBofiLGsWQM_X69HlA36578,1947
|
94
94
|
sticker_convert/utils/media/format_verify.py,sha256=Xf94jyqk_6M9IlFGMy0wYIgQKn_yg00nD4XW0CgAbew,5732
|
95
|
-
sticker_convert-2.
|
96
|
-
sticker_convert-2.
|
97
|
-
sticker_convert-2.
|
98
|
-
sticker_convert-2.
|
99
|
-
sticker_convert-2.
|
100
|
-
sticker_convert-2.
|
95
|
+
sticker_convert-2.8.0.dist-info/LICENSE,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
|
96
|
+
sticker_convert-2.8.0.dist-info/METADATA,sha256=M1OIHNyHntzmytdfECLmPnQ3r_veJG2y5W3JcoE845o,49132
|
97
|
+
sticker_convert-2.8.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
98
|
+
sticker_convert-2.8.0.dist-info/entry_points.txt,sha256=MNJ7XyC--ugxi5jS1nzjDLGnxCyLuaGdsVLnJhDHCqs,66
|
99
|
+
sticker_convert-2.8.0.dist-info/top_level.txt,sha256=r9vfnB0l1ZnH5pTH5RvkobnK3Ow9m0RsncaOMAtiAtk,16
|
100
|
+
sticker_convert-2.8.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|