• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Jaded-Encoding-Thaumaturgy / vs-tools / 13116114213

03 Feb 2025 02:47PM UTC coverage: 58.14% (-0.02%) from 58.162%
13116114213

Pull #171

github

web-flow
Merge b698ef0de into cca93572c
Pull Request #171: Use `with x.get_frame` instead of `x.get_frame` globally

6 of 16 new or added lines in 7 files covered. (37.5%)

29 existing lines in 1 file now uncovered.

2832 of 4871 relevant lines covered (58.14%)

0.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

32.68
/vstools/utils/misc.py
1
from __future__ import annotations
1✔
2

3
from fractions import Fraction
1✔
4
from math import floor
1✔
5
from types import TracebackType
1✔
6
from typing import Any, Callable, Iterable, Sequence, TypeVar, overload
1✔
7

8
import vapoursynth as vs
1✔
9
from stgpytools import MISSING
1✔
10

11
from ..enums import Align, BaseAlign
1✔
12
from ..exceptions import InvalidSubsamplingError
1✔
13
from .info import get_video_format
1✔
14

15
__all__ = [
1✔
16
    'change_fps',
17

18
    'match_clip',
19

20
    'padder',
21

22
    'pick_func_stype',
23

24
    'set_output'
25
]
26

27

28
def change_fps(clip: vs.VideoNode, fps: Fraction) -> vs.VideoNode:
1✔
29
    """
30
    Convert the framerate of a clip.
31

32
    This is different from AssumeFPS as this will actively adjust
33
    the framerate of a clip, rather than simply set the framerate properties.
34

35
    :param clip:        Input clip.
36
    :param fps:         Framerate to convert the clip to. Must be a Fraction.
37

38
    :return:            Clip with the framerate converted and frames adjusted as necessary.
39
    """
40

41
    src_num, src_den = clip.fps_num, clip.fps_den
×
42

43
    dest_num, dest_den = fps.as_integer_ratio()
×
44

45
    if (dest_num, dest_den) == (src_num, src_den):
×
46
        return clip
×
47

48
    factor = (dest_num / dest_den) * (src_den / src_num)
×
49

50
    new_fps_clip = clip.std.BlankClip(
×
51
        length=floor(clip.num_frames * factor), fpsnum=dest_num, fpsden=dest_den
52
    )
53

54
    return new_fps_clip.std.FrameEval(lambda n: clip[round(n / factor)])
×
55

56

57
def match_clip(
1✔
58
    clip: vs.VideoNode, ref: vs.VideoNode, dimensions: bool = True,
59
    vformat: bool = True, matrices: bool = True, length: bool = False
60
) -> vs.VideoNode:
61
    """
62
    Try to match the formats, dimensions, etc. of a reference clip to match the original clip.
63

64
    :param clip:            Original clip.
65
    :param ref:             Reference clip.
66
    :param dimensions:      Whether to adjust the dimensions of the reference clip to match the original clip.
67
                            If True, uses resize.Bicubic to resize the image. Default: True.
68
    :param vformat:         Whether to change the reference clip's format to match the original clip's.
69
                            Default: True.
70
    :param matrices:        Whether to adjust the Matrix, Transfer, and Primaries of the reference clip
71
                            to match the original clip. Default: True.
72
    :param length:          Whether to adjust the length of the reference clip to match the original clip.
73
    """
74
    from ..enums import Matrix, Primaries, Transfer
×
75
    from ..functions import check_variable
×
76

77
    assert check_variable(clip, match_clip)
×
78
    assert check_variable(ref, match_clip)
×
79

80
    clip = clip * ref.num_frames if length else clip
×
81
    clip = clip.resize.Bicubic(ref.width, ref.height) if dimensions else clip
×
82

83
    if vformat:
×
84
        assert ref.format
×
85
        clip = clip.resize.Bicubic(format=ref.format.id, matrix=Matrix.from_video(ref))
×
86

87
    if matrices:
×
NEW
88
        with ref.get_frame(0) as ref_frame:
×
NEW
89
            clip = clip.std.SetFrameProps(
×
90
                _Matrix=Matrix(ref_frame), _Transfer=Transfer(ref_frame), _Primaries=Primaries(ref_frame)
91
            )
92

93
    return clip.std.AssumeFPS(fpsnum=ref.fps.numerator, fpsden=ref.fps.denominator)
×
94

95

96
class _padder:
1✔
97
    """Pad out the pixels on the sides by the given amount of pixels."""
98

99
    def _base(self, clip: vs.VideoNode, left: int = 0, right: int = 0, top: int = 0, bottom: int = 0) -> tuple[
1✔
100
        int, int, vs.VideoFormat, int, int
101
    ]:
102
        from ..functions import check_variable
×
103

104
        assert check_variable(clip, 'padder')
×
105

106
        width = clip.width + left + right
×
107
        height = clip.height + top + bottom
×
108

109
        fmt = get_video_format(clip)
×
110

111
        w_sub, h_sub = 1 << fmt.subsampling_w, 1 << fmt.subsampling_h
×
112

113
        if width % w_sub and height % h_sub:
×
114
            raise InvalidSubsamplingError(
×
115
                'padder', fmt, 'Values must result in a mod congruent to the clip\'s subsampling ({subsampling})!'
116
            )
117

118
        return width, height, fmt, w_sub, h_sub
×
119

120
    def MIRROR(self, clip: vs.VideoNode, left: int = 0, right: int = 0, top: int = 0, bottom: int = 0) -> vs.VideoNode:
1✔
121
        """
122
        Pad a clip with reflect mode. This will reflect the clip on each side.
123

124
        Visual example:
125

126
        .. code-block:: python
127

128
            >>> |ABCDE
129
            >>> padder.MIRROR(left=3)
130
            >>> CBA|ABCDE
131

132
        :param clip:        Input clip.
133
        :param left:        Padding added to the left side of the clip.
134
        :param right:       Padding added to the right side of the clip.
135
        :param top:         Padding added to the top side of the clip.
136
        :param bottom:      Padding added to the bottom side of the clip.
137

138
        :return:            Padded clip with reflected borders.
139
        """
140

141
        from ..utils import core
×
142

143
        width, height, *_ = self._base(clip, left, right, top, bottom)
×
144

145
        return core.resize.Point(
×
146
            clip.std.CopyFrameProps(clip.std.BlankClip()), width, height,
147
            src_top=-top, src_left=-left,
148
            src_width=width, src_height=height
149
        ).std.CopyFrameProps(clip)
150

151
    def REPEAT(self, clip: vs.VideoNode, left: int = 0, right: int = 0, top: int = 0, bottom: int = 0) -> vs.VideoNode:
1✔
152
        """
153
        Pad a clip with repeat mode. This will simply repeat the last row/column till the end.
154

155
        Visual example:
156

157
        .. code-block:: python
158

159
            >>> |ABCDE
160
            >>> padder.REPEAT(left=3)
161
            >>> AAA|ABCDE
162

163
        :param clip:        Input clip.
164
        :param left:        Padding added to the left side of the clip.
165
        :param right:       Padding added to the right side of the clip.
166
        :param top:         Padding added to the top side of the clip.
167
        :param bottom:      Padding added to the bottom side of the clip.
168

169
        :return:            Padded clip with repeated borders.
170
        """
171

172
        *_, fmt, w_sub, h_sub = self._base(clip, left, right, top, bottom)
×
173

174
        padded = clip.std.AddBorders(left, right, top, bottom)
×
175

176
        right, bottom = clip.width + left, clip.height + top
×
177

178
        pads = [
×
179
            (left, right, top, bottom),
180
            (left // w_sub, right // w_sub, top // h_sub, bottom // h_sub),
181
        ][:fmt.num_planes]
182

183
        return padded.akarin.Expr([
×
184
            """
185
                X {left} < L! Y {top} < T! X {right} > R! Y {bottom} > B!
186

187
                T@ B@ or L@ R@ or and
188
                    L@
189
                        T@ {left} {top}  x[] {left} {bottom} x[] ?
190
                        T@ {right} {top} x[] {right} {bottom} x[] ?
191
                    ?
192
                    L@
193
                        {left} Y x[]
194
                        R@
195
                            {right} Y x[]
196
                            T@
197
                                X {top} x[]
198
                                B@
199
                                    X {bottom} x[]
200
                                    x
201
                                ?
202
                            ?
203
                        ?
204
                    ?
205
                ?
206
            """.format(
207
                left=l_, right=r_ - 1, top=t_, bottom=b_ - 1
208
            )
209
            for l_, r_, t_, b_ in pads
210
        ])
211

212
    def COLOR(
1✔
213
        self, clip: vs.VideoNode, left: int = 0, right: int = 0, top: int = 0, bottom: int = 0,
214
        color: int | float | bool | None | Sequence[int | float | bool | None] = (False, MISSING)
215
    ) -> vs.VideoNode:
216
        """
217
        Pad a clip with a constant color.
218

219
        Visual example:
220

221
        .. code-block:: python
222

223
            >>> |ABCDE
224
            >>> padder.COLOR(left=3, color=Z)
225
            >>> ZZZ|ABCDE
226

227
        :param clip:        Input clip.
228
        :param left:        Padding added to the left side of the clip.
229
        :param right:       Padding added to the right side of the clip.
230
        :param top:         Padding added to the top side of the clip.
231
        :param bottom:      Padding added to the bottom side of the clip.
232
        :param color:       Constant color that should be added on the sides:
233
                                * number: This will be treated as such and not converted or clamped.
234
                                * False: Lowest value for this clip format and color range.
235
                                * True: Highest value for this clip format and color range.
236
                                * None: Neutral value for this clip format.
237
                                * MISSING: Automatically set to False if RGB, else None.
238

239
        :return:            Padded clip with colored borders.
240
        """
241

242
        from ..functions import normalize_seq
×
243
        from ..utils import get_lowest_values, get_neutral_values, get_peak_values
×
244

245
        self._base(clip, left, right, top, bottom)
×
246

247
        def _norm(colr: int | float | bool | None | MISSING) -> Sequence[int | float]:
×
248
            if MISSING:
×
249
                colr = False if clip.format.color_family is vs.RGB else None
×
250

251
            if colr is False:
×
252
                return get_lowest_values(clip, clip)
×
253

254
            if colr is True:
×
255
                return get_peak_values(clip, clip)
×
256

257
            if colr is None:
×
258
                return get_neutral_values(clip)
×
259

260
            return normalize_seq(colr, clip.format.num_planes)
×
261

262
        if not isinstance(color, Sequence):
×
263
            norm_colors = _norm(color)
×
264
        else:
265
            assert clip.format
×
266
            norm_colors = [_norm(c)[i] for i, c in enumerate(normalize_seq(color, clip.format.num_planes))]
×
267

268
        return clip.std.AddBorders(left, right, top, bottom, norm_colors)
×
269

270
    @classmethod
1✔
271
    def _get_sizes_crop_scale(
1✔
272
        cls, sizes: tuple[int, int] | vs.VideoNode, crop_scale: float | tuple[float, float]
273
    ) -> tuple[tuple[int, int], tuple[int, int]]:
274
        if isinstance(sizes, vs.VideoNode):
×
275
            sizes = (sizes.width, sizes.height)
×
276

277
        if not isinstance(crop_scale, tuple):
×
278
            crop_scale = (crop_scale, crop_scale)
×
279

280
        return sizes, crop_scale  # type: ignore[return-value]
×
281

282
    @classmethod
1✔
283
    def _crop_padding(cls, crop_scale: tuple[int, int]) -> tuple[int, int, int, int]:
1✔
284
        return tuple(crop_scale[0 if i < 2 else 1] for i in range(4))  # type: ignore
×
285

286
    @classmethod
1✔
287
    def mod_padding(
1✔
288
        cls, sizes: tuple[int, int] | vs.VideoNode, mod: int = 16, min: int = 4, align: Align = Align.MIDDLE_CENTER
289
    ) -> tuple[int, int, int, int]:
290
        sizes, _ = cls._get_sizes_crop_scale(sizes, 1)
×
291
        ph, pv = (mod - (((x + min * 2) - 1) % mod + 1) for x in sizes)
×
292
        left, top = floor(ph / 2), floor(pv / 2)
×
293
        left, right, top, bottom = tuple(x + min for x in (left, ph - left, top, pv - top))
×
294

295
        if align & BaseAlign.TOP:
×
296
            bottom += top
×
297
            top = 0
×
298
        elif align & BaseAlign.BOTTOM:
×
299
            top += bottom
×
300
            bottom = 0
×
301

302
        if align & BaseAlign.LEFT:
×
303
            right += left
×
304
            left = 0
×
305
        elif align & BaseAlign.RIGHT:
×
306
            left += right
×
307
            right = 0
×
308

309
        return left, right, top, bottom
×
310

311
    @classmethod
1✔
312
    def mod_padding_crop(
1✔
313
        cls, sizes: tuple[int, int] | vs.VideoNode, mod: int = 16, min: int = 4,
314
        crop_scale: float | tuple[float, float] = 2, align: Align = Align.MIDDLE_CENTER
315
    ) -> tuple[tuple[int, int, int, int], tuple[int, int, int, int]]:
316
        sizes, crop_scale = cls._get_sizes_crop_scale(sizes, crop_scale)
×
317
        padding = cls.mod_padding(sizes, mod, min, align)
×
318
        return padding, tuple(x * crop_scale[0 if i < 2 else 1] for x, i in enumerate(padding))  # type: ignore
×
319

320
    class ctx:
1✔
321
        class padder_ctx:
1✔
322
            def __init__(self, ctx: _padder.ctx) -> None:
1✔
323
                self.ctx = ctx
×
324
                self.pad_ops = list[tuple[tuple[int, int, int, int], tuple[int, int]]]()
×
325

326
            def COLOR(
1✔
327
                self, clip: vs.VideoNode,
328
                color: int | float | bool | None | Sequence[int | float | bool | None] = (False, None)
329
            ) -> vs.VideoNode:
330
                padding = padder.mod_padding(clip, self.ctx.mod, self.ctx.min, self.ctx.align)
×
331
                out = padder.COLOR(clip, *padding, color)
×
332
                self.pad_ops.append((padding, (out.width, out.height)))
×
333
                return out
×
334

335
            def REPEAT(self, clip: vs.VideoNode) -> vs.VideoNode:
1✔
336
                padding = padder.mod_padding(clip, self.ctx.mod, self.ctx.min, self.ctx.align)
×
337
                out = padder.REPEAT(clip, *padding)
×
338
                self.pad_ops.append((padding, (out.width, out.height)))
×
339
                return out
×
340

341
            def MIRROR(self, clip: vs.VideoNode) -> vs.VideoNode:
1✔
342
                padding = padder.mod_padding(clip, self.ctx.mod, self.ctx.min, self.ctx.align)
×
343
                out = padder.MIRROR(clip, *padding)
×
344
                self.pad_ops.append((padding, (out.width, out.height)))
×
345
                return out
×
346

347
            def CROP(self, clip: vs.VideoNode, crop_scale: float | tuple[float, float] | None = None) -> vs.VideoNode:
1✔
348
                (padding, sizes) = self.pad_ops.pop(0)
×
349

350
                if crop_scale is None:
×
351
                    crop_scale = (clip.width / sizes[0], clip.height / sizes[1])
×
352

353
                crop_pad = padder._crop_padding(padder._get_sizes_crop_scale(clip, crop_scale)[1])
×
354

355
                return clip.std.Crop(*(x * y for x, y in zip(padding, crop_pad)))
×
356

357
        def __init__(self, mod: int = 8, min: int = 0, align: Align = Align.MIDDLE_CENTER) -> None:
1✔
358
            self.mod = mod
×
359
            self.min = min
×
360
            self.align = align
×
361

362
        def __enter__(self) -> padder_ctx:
1✔
363
            return self.padder_ctx(self)
×
364

365
        def __exit__(
1✔
366
            self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
367
        ) -> None:
368
            return None
×
369

370

371
padder = _padder()
1✔
372

373
FINT = TypeVar('FINT', bound=Callable[..., vs.VideoNode])
1✔
374
FFLOAT = TypeVar('FFLOAT', bound=Callable[..., vs.VideoNode])
1✔
375

376

377
def pick_func_stype(clip: vs.VideoNode, func_int: FINT, func_float: FFLOAT) -> FINT | FFLOAT:
1✔
378
    """
379
    Pick the function matching the sample type of the clip's format.
380

381
    :param clip:        Input clip.
382
    :param func_int:    Function to run on integer clips.
383
    :param func_float:  Function to run on float clips.
384

385
    :return:            Function matching the sample type of your clip's format.
386
    """
387

388
    assert clip.format
×
389

390
    return func_float if clip.format.sample_type == vs.FLOAT else func_int
×
391

392

393
@overload
1✔
394
def set_output(
1✔
395
    node: vs.VideoNode,
396
    index: int = ...,
397
    /,
398
    *,
399
    alpha: vs.VideoNode | None = ...,
400
    **kwargs: Any
401
) -> None:
402
    ...
×
403

404

405
@overload
1✔
406
def set_output(
1✔
407
    node: vs.VideoNode,
408
    name: str | bool | None = ...,
409
    /,
410
    *,
411
    alpha: vs.VideoNode | None = ...,
412
    **kwargs: Any
413
) -> None:
414
    ...
×
415

416

417
@overload
1✔
418
def set_output(
1✔
419
    node: vs.VideoNode,
420
    index: int = ..., name: str | bool | None = ...,
421
    /,
422
    alpha: vs.VideoNode | None = ...,
423
    **kwargs: Any
424
) -> None:
425
    ...
×
426

427

428
@overload
1✔
429
def set_output(
1✔
430
    node: vs.AudioNode,
431
    index: int = ...,
432
    /,
433
    **kwargs: Any
434
) -> None:
435
    ...
×
436

437

438
@overload
1✔
439
def set_output(
1✔
440
    node: vs.AudioNode,
441
    name: str | bool | None = ...,
442
    /,
443
    **kwargs: Any
444
) -> None:
445
    ...
×
446

447

448
@overload
1✔
449
def set_output(
1✔
450
    node: vs.AudioNode,
451
    index: int = ..., name: str | bool | None = ...,
452
    /,
453
    **kwargs: Any
454
) -> None:
455
    ...
×
456

457

458
@overload
1✔
459
def set_output(
1✔
460
    node: Iterable[vs.VideoNode | Iterable[vs.VideoNode | Iterable[vs.VideoNode]]],
461
    index: int | Sequence[int] = ...,
462
    /,
463
    **kwargs: Any
464
) -> None:
465
    ...
×
466

467

468
@overload
1✔
469
def set_output(
1✔
470
    node: Iterable[vs.VideoNode | Iterable[vs.VideoNode | Iterable[vs.VideoNode]]],
471
    name: str | bool | None = ...,
472
    /,
473
    **kwargs: Any
474
) -> None:
475
    ...
×
476

477

478
@overload
1✔
479
def set_output(
1✔
480
    node: Iterable[vs.VideoNode | Iterable[vs.VideoNode | Iterable[vs.VideoNode]]],
481
    index: int | Sequence[int] = ..., name: str | bool | None = ...,
482
    /,
483
    **kwargs: Any
484
) -> None:
485
    ...
×
486

487

488
@overload
1✔
489
def set_output(
1✔
490
    node: Iterable[vs.AudioNode | Iterable[vs.AudioNode | Iterable[vs.AudioNode]]],
491
    index: int | Sequence[int] = ...,
492
    /,
493
    **kwargs: Any
494
) -> None:
495
    ...
×
496

497

498
@overload
1✔
499
def set_output(
1✔
500
    node: Iterable[vs.AudioNode | Iterable[vs.AudioNode | Iterable[vs.AudioNode]]],
501
    name: str | bool | None = ...,
502
    /,
503
    **kwargs: Any
504
) -> None:
505
    ...
×
506

507

508
@overload
1✔
509
def set_output(
1✔
510
    node: Iterable[vs.AudioNode | Iterable[vs.AudioNode | Iterable[vs.AudioNode]]],
511
    index: int | Sequence[int] = ..., name: str | bool | None = ...,
512
    /,
513
    **kwargs: Any
514
) -> None:
515
    ...
×
516

517

518
@overload
1✔
519
def set_output(
1✔
520
    node: vs.RawNode | Iterable[vs.RawNode | Iterable[vs.RawNode | Iterable[vs.RawNode]]],
521
    index: int | Sequence[int] = ..., name: str | bool | None = ...,
522
    /,
523
    **kwargs: Any
524
) -> None:
525
    ...
×
526

527

528
def set_output(
1✔
529
    node: vs.RawNode | Iterable[vs.RawNode | Iterable[vs.RawNode | Iterable[vs.RawNode]]],
530
    index_or_name: int | Sequence[int] | str | bool | None = None, name: str | bool | None = None,
531
    /,
532
    alpha: vs.VideoNode | None = None,
533
    **kwargs: Any
534
) -> None:
535
    """Set output node with optional name, and if available, use vspreview set_output.
536

537
    :param node:            Output node
538
    :param index:           Index number, defaults to current maximum index number + 1 or 0 if no ouput exists yet
539
    :param name:            Node's name, defaults to variable name
540
    :param alpha:           Alpha planes node, defaults to None
541
    """
542
    from ..functions import flatten, to_arr
×
543

544
    if isinstance(index_or_name, (str, bool)):
×
545
        index = None
×
546
        # Backward compatible with older api
547
        if isinstance(name, vs.VideoNode):
×
548
            alpha = name  # type: ignore[unreachable]
×
549
        name = index_or_name
×
550
    else:
551
        index = index_or_name
×
552

553
    ouputs = vs.get_outputs()
×
554
    nodes = list(flatten(node))
×
555

556
    index = to_arr(index) if index is not None else [max(ouputs, default=-1) + 1]
×
557

558
    while len(index) < len(nodes):
×
559
        index.append(index[-1] + 1)
×
560

561
    try:
×
562
        from vspreview import set_output as vsp_set_output
×
563
        vsp_set_output(nodes, index, name, alpha=alpha, f_back=2, force_preview=True, **kwargs)
×
564
    except ModuleNotFoundError:
×
565
        for idx, n in zip(index, nodes):
×
566
            n.set_output(idx)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc