backend/tests/test_ass_drawing_in_arguments.py
Rama master · réplica de lectura del repositorio original.
"""``\\p1`` inside another tag's arguments, measured against libass.
An external review of 1.15.0-rc.17 (2026-09-30) reported that these events
render visible dialogue but are read as drawings and deleted. Measured on the
PC with ffmpeg 8.0.1 / libass (320x180 frame, lit pixels > 40 of 255), the
lexer already agreed with the renderer on every one of them; the report
describes the regex reading that 1.14.4 replaced. The reference table in
``fixtures/ass_libass_0173.json`` has no ``\\t`` case at all, so the transform
rule was never pinned against pixels until now:
2128 px {\\p1(0)}Good morning.{\\p0} paren arg wins: p0
2128 px {\\t(0,1000,2,3,\\p1)}Good morning.{\\p0} 5 args: no transform
0 px {\\t(0,1000,2,\\p1)}Good morning.{\\p0} 4 args: drawing on
0 px {\\t(\\p1)}Good morning.{\\p0} 1 arg: drawing on
2128 px {\\zzz(\\p1)}Good morning.{\\p0} unknown tag
0 px {\\p1}m 0 0 l 1 1{\\c&H0000FF&}Good morning.{\\p0}
the text is inside the drawing
"""
import os
import shutil
import subprocess
import pytest
from ass_utils import text_outside_drawing
from subtitle_sanitizer import sanitize_ass_content
LINE = "Good morning."
MEASURED = [
(r"{\p1(0)}Good morning.{\p0}", True),
(r"{\t(0,1000,2,3,\p1)}Good morning.{\p0}", True),
(r"{\t(0,1000,2,\p1)}Good morning.{\p0}", False),
(r"{\t(\p1)}Good morning.{\p0}", False),
(r"{\zzz(\p1)}Good morning.{\p0}", True),
(r"{\p1}m 0 0 l 1 1{\c&H0000FF&}Good morning.{\p0}", False),
]
HEADER = """[Script Info]
ScriptType: v4.00+
PlayResX: 320
PlayResY: 180
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,40,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,5,10,10,10,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
"""
def _event(text: str) -> str:
return f"Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,{text}\n"
@pytest.mark.parametrize("text, visible", MEASURED)
def test_the_lexer_reads_the_line_as_libass_renders_it(text, visible):
assert (text_outside_drawing(text) == LINE) is visible
@pytest.mark.parametrize("text, visible", [m for m in MEASURED if m[1]])
def test_the_sanitizer_keeps_visible_dialogue(text, visible):
cleaned = sanitize_ass_content((HEADER + _event(text)).encode()).decode()
assert text in cleaned
def test_tags_inside_a_removed_span_keep_applying():
"""Only geometry leaves; colour, alignment and position stay in order."""
text = r"{\p1}m 0 0 l 1 1{\c&H0000FF&\an8\pos(10,20)}l 2 2{\p0}Hello"
cleaned = sanitize_ass_content((HEADER + _event(text)).encode()).decode()
assert r"{\p1}{\c&H0000FF&\an8\pos(10,20)}{\p0}Hello" in cleaned
def _has_libass() -> bool:
if shutil.which("ffmpeg") is None:
return False
out = subprocess.run(["ffmpeg", "-hide_banner", "-filters"], capture_output=True, text=True)
return " subtitles " in out.stdout
def _lit_pixels(tmp_path, text: str) -> int:
ass = tmp_path / "s.ass"
ass.write_text(HEADER + _event(text), encoding="utf-8")
# The subtitles filter parses ':' and '\' itself — pass a relative path.
rel = os.path.relpath(ass).replace("\\", "/")
out = subprocess.run(
[
"ffmpeg", "-v", "error", "-f", "lavfi", "-i", "color=black:s=320x180:d=5",
"-ss", "1", "-vf", f"subtitles={rel}", "-frames:v", "1",
"-f", "rawvideo", "-pix_fmt", "gray", "-",
],
capture_output=True,
check=True,
) # fmt: skip
return sum(1 for b in out.stdout if b > 40)
@pytest.mark.skipif(not _has_libass(), reason="needs ffmpeg built with libass")
@pytest.mark.parametrize("text, visible", MEASURED)
def test_the_renderer_still_agrees(tmp_path, monkeypatch, text, visible):
monkeypatch.chdir(tmp_path)
assert (_lit_pixels(tmp_path, text) > 0) is visible