Ctrl K
ComunidadGuías…

backend/tests/test_ass_drawing_semantics.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""One reading of ``\\p`` tags, and it has to be the renderer's.

Sandbox VM, 1.14.4-rc.10: a differential harness put 47 override forms through
the ffmpeg/libass shipped in our own release image and through the product
helper, in both initial drawing states, and found 16 disagreements. Separately
it showed the security sanitizer using a *different*, older reading — and that
one deletes visible dialogue outright.
"""

from __future__ import annotations

import pytest

#: Spelled with chr() rather than written into the string literals: as literal
#: characters these are invisible, and two of the cases below would look like
#: exact duplicates of each other in any diff.
NBSP = chr(0x00A0)
EM_SPACE = chr(0x2003)
ARABIC_INDIC_ONE = chr(0x0661)
FULLWIDTH_ONE = chr(0xFF11)


@pytest.mark.parametrize(
    "tag_block, before, expected",
    [
        # libass accepts a leading plus; rc.10 allowed only a minus, so the
        # coordinates of a {\p+1} drawing were handed to the model.
        (r"{\p+1}", False, True),
        (r"{\p +1}", False, True),
        # The last \p in one block wins, plus sign included.
        (r"{\p1\p0\p+1}", False, True),
        (r"{\p1\p0}", False, False),
        # A no-break space is not ASCII whitespace, so the argument is not
        # read as a number at all and drawing ends up off — rc.10 accepted it
        # via Python's \s and skipped the visible dialogue that followed.
        (r"{\p" + NBSP + "1}", False, False),
        (r"{\p" + EM_SPACE + "1}", False, False),
        # Python's \d and int() take these for ones; libass does not.
        (r"{\p" + ARABIC_INDIC_ONE + "}", False, False),
        (r"{\p" + FULLWIDTH_ONE + "}", False, False),
        # An unparseable rest reads as 0, which turns drawing off rather than
        # leaving the tag unread.
        (r"{\pfoo}", True, False),
        (r"{\pBO3}", True, False),
        # ... but the real lowercase \pbo and \pos tags are left alone.
        (r"{\pbo3}", True, True),
        (r"{\pbo3}", False, False),
        (r"{\pos(10,20)}", True, True),
        (r"{\pos(10,20)}", False, False),
        # The canonical forms keep working.
        (r"{\p1}", False, True),
        (r"{\p0}", True, False),
        (r"{\p01}", False, True),
        (r"{\p12}", False, True),
        (r"{\p-1}", True, False),
        (r"{\p}", True, False),
        (r"{\an8\i1}", True, True),
        (r"{\an8\i1}", False, False),
        # Whitespace may sit between the backslash and the tag name.
        (r"{\ p1}", False, True),
        (r"{\  p1}", False, True),
        ("{" + chr(92) + chr(9) + "p1}", False, True),
        (r"{\ p0}", True, False),
        (r"{\ P1}", False, False),
        (r"{\ pbo3}", True, True),
        # One opening parenthesis is skipped, a second one is not.
        (r"{\p(1)}", False, True),
        (r"{\p(0)}", True, False),
        (r"{\p(-1)}", True, False),
        (r"{\p(+1)}", False, True),
        (r"{\p( 1 )}", False, True),
        (r"{\p((1))}", False, False),
        (r"{\p(1}", False, True),
        (r"{\p(foo)}", True, False),
        # Digits are read greedily and whatever follows them is ignored.
        (r"{\p1)}", False, True),
        (r"{\p1x}", False, True),
        (r"{\p0x}", True, False),
        (r"{\p1 2}", False, True),
        (r"{\ p(1)}", False, True),
    ],
)
def test_drawing_state_after(tag_block, before, expected):
    from ass_drawing import drawing_state_after

    assert drawing_state_after(tag_block, before) is expected


def test_a_huge_argument_does_not_raise():
    """A 5000-digit argument failed the whole file, ordinary lines included.

    ``int()`` refuses more than 4300 digits in Python 3.12, and the helper let
    that ValueError escape, so one malformed event cost every dialogue in the
    file its translation. Whether a number is positive needs no conversion.
    """
    from ass_drawing import drawing_state_after

    assert drawing_state_after("{\\p" + "9" * 5000 + "}", False) is True
    assert drawing_state_after("{\\p" + "0" * 5000 + "}", True) is False


@pytest.mark.parametrize(
    "text, expected",
    [
        ("Good morning.", True),
        ("", False),
        ("   ", False),
        # Layout escapes are not sentences. A lone \N counted as a stretch of
        # dialogue, so a caption with a leading line break looked like two
        # sentences with a drawing between them and was skipped entirely.
        (r"\N", False),
        (r"\h", False),
        (r"\n", False),
        (r"\N\h", False),
        (r"Good\Nmorning.", True),
    ],
)
def test_holds_language(text, expected):
    from ass_drawing import holds_language

    assert holds_language(text) is expected


class TestSanitizerUsesTheSameReading:
    """The sanitizer deleted visible dialogue because it read \\p differently.

    Both cases below report ``success=true`` from the flow while the saved
    event is empty — confirmed by the VM against the renderer: the expected
    frame shows "Guten Morgen.", the actual frame is black.
    """

    def test_uppercase_p_is_not_an_opener(self):
        from subtitle_sanitizer import strip_drawing_blocks

        assert strip_drawing_blocks(r"{\P1}Guten Morgen.{\p0}") == r"{\P1}Guten Morgen.{\p0}"

    def test_a_block_that_turns_drawing_off_again_is_not_an_opener(self):
        from subtitle_sanitizer import strip_drawing_blocks

        assert strip_drawing_blocks(r"{\p1\p0}Guten Morgen.{\p0}") == r"{\p1\p0}Guten Morgen.{\p0}"

    def test_a_real_drawing_span_is_still_removed(self):
        """The security control itself must not weaken."""
        from subtitle_sanitizer import strip_drawing_blocks

        assert (
            strip_drawing_blocks(r"{\p1}m 0 0 l 10 10{\p0}Guten Morgen.")
            == r"{\p1}{\p0}Guten Morgen."
        )

    def test_an_unclosed_opener_keeps_its_geometry(self):
        """Unclosed is how an ASS drawing is normally written.

        Removing it looked right — one such span lights 224 000 of 230 400
        pixels, which is what this filter calls an overlay — until the library
        was measured: in one typesetting-heavy episode 1 045 of 2 752 dialogue
        lines carry a ``\\p`` opener and not one of them is ever closed; across
        4 000 sampled files, 932 (23 %) and 2.7 million events. The geometry is
        the whole event, so a closer would be pointless, and stripping it would
        delete the signs and masks of a quarter of the library from every file
        Sublarr writes.
        """
        from subtitle_sanitizer import strip_drawing_blocks

        assert strip_drawing_blocks(r"{\p1}m 0 0 l 10 10") == r"{\p1}m 0 0 l 10 10"
        assert strip_drawing_blocks(r"{\an8\p1}m 0 0 l 10 10") == r"{\an8\p1}m 0 0 l 10 10"

    def test_text_before_an_unclosed_opener_is_untouched(self):
        from subtitle_sanitizer import strip_drawing_blocks

        assert strip_drawing_blocks(r"Guten Morgen.{\p1}m 0 0") == r"Guten Morgen.{\p1}m 0 0"

    def test_a_closed_span_is_still_removed_on_the_same_line(self):
        """The rule that did not change: a complete span still goes."""
        from subtitle_sanitizer import strip_drawing_blocks

        assert strip_drawing_blocks(r"{\p1}m 0 0{\p0}Guten Morgen.") == r"{\p1}{\p0}Guten Morgen."

    def test_the_fast_path_uses_the_same_reading_as_the_walk(self):
        """A shortcut that asks a different question is a hole in the filter.

        ``strip_drawing_blocks`` returned early unless the literal two
        characters ``\\p`` appeared. Once whitespace after the backslash became
        an opener, ``{\\ p1}`` no longer contained that substring — so the file
        was handed back untouched and a full-screen overlay went to disk. Found
        by the acceptance probe against the rc.14 image, not by this suite.
        """
        from subtitle_sanitizer import strip_drawing_blocks

        # A closed span, because an unclosed one is deliberately kept — what is
        # under test here is that the shortcut sees the opener at all.
        assert strip_drawing_blocks(r"{\ p1}m 0 0{\p0}Guten Morgen.") == r"{\ p1}{\p0}Guten Morgen."
        assert strip_drawing_blocks(r"{\ p1}m 0 0 l 640 0{\p0}") == r"{\ p1}{\p0}"

    def test_text_without_any_drawing_tag_is_returned_unchanged(self):
        from subtitle_sanitizer import strip_drawing_blocks

        assert strip_drawing_blocks("Guten Morgen.") == "Guten Morgen."
        assert strip_drawing_blocks(r"{\an8\i1}Guten Morgen.") == r"{\an8\i1}Guten Morgen."

    def test_a_plus_signed_opener_is_removed_too(self):
        from subtitle_sanitizer import strip_drawing_blocks

        assert strip_drawing_blocks(r"{\p+1}m 0 0{\p0}Guten Morgen.") == r"{\p+1}{\p0}Guten Morgen."


class TestRemovingADrawingKeepsTheRestOfItsBlock:
    """Only the drawing goes; the overrides sharing its block have to stay.

    A sign is usually authored as one event — ``{\\an8\\pos(...)\\p1}`` geometry
    ``{\\p0}`` caption — so deleting the opening block whole took the caption's
    position with the drawing and dropped it to the default bottom-centre.
    Sandbox VM, 1.14.4-rc.10. Older than this release series.
    """

    def test_positioning_in_the_opening_block_survives(self):
        from subtitle_sanitizer import strip_drawing_blocks

        assert (
            strip_drawing_blocks(r"{\an8\p1}m 0 0 l 10 10{\p0}Guten Morgen.")
            == r"{\an8\p1}{\p0}Guten Morgen."
        )

    def test_overrides_in_the_closing_block_survive(self):
        from subtitle_sanitizer import strip_drawing_blocks

        assert (
            strip_drawing_blocks(r"{\p1}m 0 0{\p0\i1}Guten Morgen.")
            == r"{\p1}{\p0\i1}Guten Morgen."
        )

    def test_drawing_switches_survive_without_geometry(self):
        from subtitle_sanitizer import strip_drawing_blocks

        assert (
            strip_drawing_blocks(r"{\p1}m 0 0 l 10 10{\p0}Guten Morgen.")
            == r"{\p1}{\p0}Guten Morgen."
        )

    def test_several_overrides_keep_their_order(self):
        from subtitle_sanitizer import strip_drawing_blocks

        assert (
            strip_drawing_blocks(r"{\an8\p1\pos(10,20)}m 0 0{\p0}Guten Morgen.")
            == r"{\an8\p1\pos(10,20)}{\p0}Guten Morgen."
        )