"""字幕渲染纯函数测试 — _build_ass_style + generate_ass_subtitles.""" from __future__ import annotations from pathlib import Path import pytest from video_processing.render_subtitles import ( _build_ass_style, _escape_ass_text, _format_ass_time, _hex_to_ass_color, _position_to_ass_alignment, generate_ass_subtitles, ) class TestHexToAssColor: """_hex_to_ass_color 颜色转换测试.""" def test_white(self): """白色 #FFFFFF → &HFFFFFF (ASS BGR格式).""" assert _hex_to_ass_color("#FFFFFF") == "&HFFFFFF" def test_black(self): """黑色 #000000 → &H000000.""" assert _hex_to_ass_color("#000000") == "&H000000" def test_red(self): """红色 #FF0000 → ASS是BGR顺序 → &H0000FF.""" assert _hex_to_ass_color("#FF0000") == "&H0000FF" def test_blue(self): """蓝色 #0000FF → BGR → &HFF0000.""" assert _hex_to_ass_color("#0000FF") == "&HFF0000" def test_green(self): """绿色 #00FF00 → BGR → &H00FF00.""" assert _hex_to_ass_color("#00FF00") == "&H00FF00" def test_lowercase(self): """小写hex也支持.""" assert _hex_to_ass_color("#ff0000") == "&H0000FF" def test_no_hash_prefix(self): """不带#的颜色.""" assert _hex_to_ass_color("FF0000") == "&H0000FF" def test_invalid_length_fallback(self): """长度不对返回默认黑色.""" assert _hex_to_ass_color("#FFF") == "&H000000" class TestPositionToAssAlignment: """_position_to_ass_alignment 位置映射测试.""" def test_top_center(self): """top → 上中(8).""" assert _position_to_ass_alignment("top") == 8 def test_bottom_center(self): """bottom → 下中(2).""" assert _position_to_ass_alignment("bottom") == 2 def test_center_middle(self): """center → 居中(5).""" assert _position_to_ass_alignment("center") == 5 def test_unknown_defaults_to_top(self): """未知位置默认顶部(8).""" assert _position_to_ass_alignment("unknown") == 8 assert _position_to_ass_alignment("top_left") == 8 assert _position_to_ass_alignment("bottom_right") == 8 def test_empty_string_defaults_to_top(self): """空字符串默认顶部.""" assert _position_to_ass_alignment("") == 8 class TestBuildAssStyle: """_build_ass_style ASS样式行构建测试.""" def test_basic_style_line(self): """基本样式行包含关键字段.""" line = _build_ass_style("Default") assert line.startswith("Style: Default,") assert "思源黑体" in line assert "48" in line # font_size def test_bold_enabled(self): """加粗时Bold=-1.""" line = _build_ass_style("Bold", bold=True) assert "Style: Bold," in line # Bold字段位置:第7个逗号分隔字段=Bold=-1 parts = line.split(",") # Name, Fontname, Fontsize, Primary, Secondary, Outline, Back, Bold, ... assert parts[7] == "-1" # Bold def test_bold_disabled(self): """不加粗时Bold=0.""" line = _build_ass_style("Normal", bold=False) parts = line.split(",") assert parts[7] == "0" def test_italic_enabled(self): """斜体时Italic=-1.""" line = _build_ass_style("Italic", italic=True) parts = line.split(",") assert parts[8] == "-1" # Italic def test_custom_font_size(self): """自定义字号.""" line = _build_ass_style("Big", font_size=72) parts = line.split(",") assert parts[2] == "72" # Fontsize def test_custom_alignment(self): """自定义对齐方式.""" line = _build_ass_style("Bottom", alignment=2) # Alignment是第16个字段(数一下) # Name, Fontname, Fontsize, Primary, Secondary, Outline, Back, Bold, Italic, Underline, Strikeout, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, ... parts = line.split(",") assert parts[18] == "2" # Alignment (0-indexed: 18) def test_outline_width(self): """描边宽度.""" line = _build_ass_style("Outline", outline_width=3.0) parts = line.split(",") assert parts[16] == "3.0" # Outline (BorderStyle后是Outline) class TestEscapeAssText: """_escape_ass_text 文本转义测试.""" def test_plain_text_unchanged(self): """普通文本不变.""" assert _escape_ass_text("hello world") == "hello world" def test_newline_converted(self): """换行符转成\\N.""" assert _escape_ass_text("line1\nline2") == "line1\\Nline2" def test_crlf_converted(self): """CRLF转成\\N.""" assert _escape_ass_text("a\r\nb") == "a\\Nb" def test_carriage_return_converted(self): """纯\\r转成\\N.""" assert _escape_ass_text("a\rb") == "a\\Nb" def test_curly_braces_replaced(self): """大括号转成圆括号(防止ASS样式注入).""" assert _escape_ass_text("{text}") == "(text)" def test_mixed_special_chars(self): """混合特殊字符.""" result = _escape_ass_text("line1\nline2 {bold}\rlast") assert "line1\\Nline2 (bold)\\Nlast" == result class TestFormatAssTime: """_format_ass_time 时间格式化测试.""" def test_zero_seconds(self): """0秒.""" assert _format_ass_time(0) == "0:00:00.00" def test_seconds_only(self): """只有秒.""" assert _format_ass_time(5.5) == "0:00:05.50" def test_minutes_and_seconds(self): """几分几秒.""" assert _format_ass_time(65.25) == "0:01:05.25" def test_hours(self): """几小时.""" assert _format_ass_time(3661.5) == "1:01:01.50" def test_always_two_decimal_places(self): """总是两位小数.""" assert _format_ass_time(1.0) == "0:00:01.00" assert _format_ass_time(1.1) == "0:00:01.10" class TestGenerateAssSubtitles: """generate_ass_subtitles ASS字幕文件生成测试.""" def test_no_subtitles_empty_file(self, tmp_path): """没有字幕生成空文件.""" output = tmp_path / "empty.ass" result = generate_ass_subtitles( output, video_width=1080, video_height=1920, video_duration=10.0, ) assert result == output assert output.exists() assert output.read_text(encoding="utf-8") == "" def test_title_only(self, tmp_path): """只有标题.""" output = tmp_path / "title.ass" result = generate_ass_subtitles( output, video_width=1080, video_height=1920, video_duration=30.0, title_text="测试标题", ) assert result == output content = output.read_text(encoding="utf-8") assert "Script Info" in content assert "PlayResX: 1080" in content assert "PlayResY: 1920" in content assert "测试标题" in content assert "V4+ Styles" in content assert "Events" in content def test_subtitle_only(self, tmp_path): """只有底部字幕.""" output = tmp_path / "sub.ass" result = generate_ass_subtitles( output, video_width=1080, video_height=1920, video_duration=15.0, subtitle_text="这是字幕", ) assert result == output content = output.read_text(encoding="utf-8") assert "这是字幕" in content assert "PlayResX: 1080" in content def test_both_title_and_subtitle(self, tmp_path): """标题+字幕都有.""" output = tmp_path / "both.ass" result = generate_ass_subtitles( output, video_width=720, video_height=1280, video_duration=20.0, title_text="大标题", subtitle_text="底部字幕", ) content = output.read_text(encoding="utf-8") assert "大标题" in content assert "底部字幕" in content # 应该有两种样式(title和subtitle) assert content.count("Style:") >= 2 def test_title_disabled_by_config(self, tmp_path): """通过config禁用标题.""" output = tmp_path / "disabled_title.ass" result = generate_ass_subtitles( output, video_width=1080, video_height=1920, video_duration=10.0, title_text="标题", title_config={"enabled": False}, subtitle_text="字幕", ) content = output.read_text(encoding="utf-8") assert "标题" not in content assert "字幕" in content def test_empty_title_text_not_rendered(self, tmp_path): """空标题文本不渲染.""" output = tmp_path / "empty_title.ass" result = generate_ass_subtitles( output, video_width=1080, video_height=1920, video_duration=10.0, title_text=" ", subtitle_text="有字幕", ) content = output.read_text(encoding="utf-8") assert "有字幕" in content def test_custom_title_color(self, tmp_path): """自定义标题颜色.""" output = tmp_path / "color.ass" result = generate_ass_subtitles( output, video_width=1080, video_height=1920, video_duration=10.0, title_text="红色标题", title_config={"color": "#FF0000"}, ) content = output.read_text(encoding="utf-8") # 红色 → ASS BGR格式 &H0000FF assert "&H0000FF" in content def test_dialogue_line_format(self, tmp_path): """Dialogue行格式正确.""" output = tmp_path / "dialogue.ass" generate_ass_subtitles( output, video_width=1080, video_height=1920, video_duration=5.0, subtitle_text="测试字幕文本", ) content = output.read_text(encoding="utf-8") assert "Dialogue:" in content assert "测试字幕文本" in content