Fixes ASS converter, adds some doc

This commit is contained in:
Guillaume Tâche
2024-10-06 17:47:36 +02:00
parent c58a8b0588
commit aab2e2a486
8 changed files with 4006 additions and 202 deletions

View File

@@ -1,6 +1,7 @@
package com.github.gtache.autosubtitle.subtitle.converter.impl;
import com.github.gtache.autosubtitle.VideoInfo;
import com.github.gtache.autosubtitle.subtitle.Bounds;
import com.github.gtache.autosubtitle.subtitle.Font;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
@@ -8,19 +9,21 @@ import com.github.gtache.autosubtitle.subtitle.converter.FormatOptions;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.converter.ParseOptions;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.impl.BoundsImpl;
import com.github.gtache.autosubtitle.subtitle.impl.FontImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
import com.github.gtache.autosubtitle.translation.Translator;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -34,7 +37,30 @@ public class ASSSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
private static final String DIALOGUE = "Dialogue:";
private static final String EVENTS_SECTION = "[Events]";
private static final String STYLES_SECTION = "[V4+ Styles]";
private static final String START_FIELD = "Start";
private static final String END_FIELD = "End";
private static final String STYLE_FIELD = "Style";
private static final String TEXT_FIELD = "Text";
private static final String NAME_FIELD = "Name";
private static final String FONTNAME_FIELD = "Fontname";
private static final String FONTSIZE_FIELD = "Fontsize";
private static final String MARGIN_L_FIELD = "MarginL";
private static final String MARGIN_R_FIELD = "MarginR";
private static final String MARGIN_V_FIELD = "MarginV";
private static final List<String> STYLE_FIELDS = List.of(NAME_FIELD, FONTNAME_FIELD, FONTSIZE_FIELD, "PrimaryColour",
"SecondaryColour", "OutlineColour", "BackColour", "Bold", "Italic",
"Underline", "StrikeOut", "ScaleX", "ScaleY", "Spacing", "Angle", "BorderStyle", "Outline", "Shadow", "Alignment",
MARGIN_L_FIELD, MARGIN_R_FIELD, MARGIN_V_FIELD, "AlphaLevel", "Encoding");
private static final Set<String> REQUIRED_STYLE_FIELDS = Set.of(NAME_FIELD, FONTNAME_FIELD, FONTSIZE_FIELD);
private static final List<String> EVENT_FIELDS = List.of("Layer", START_FIELD, END_FIELD, STYLE_FIELD, "Name", MARGIN_L_FIELD, MARGIN_R_FIELD, MARGIN_V_FIELD, "Effect", TEXT_FIELD);
private static final Set<String> REQUIRED_EVENT_FIELDS = Set.of(START_FIELD, END_FIELD, STYLE_FIELD, TEXT_FIELD);
private static final Pattern NEWLINE_PATTERN = Pattern.compile("\\n");
private static final Pattern POS_PATTERN = Pattern.compile("^\\{\\\\pos\\((?<x>\\d+),(?<y>\\d+)\\)}");
private static final Pattern START_DIALOG_PATTERN = Pattern.compile("^Dialogue: ");
private static final Pattern START_ZERO_PATTERN = Pattern.compile("^0+");
private static final Pattern PLAY_RES_Y_PATTERN = Pattern.compile("^PlayResY: (?<height>\\d+)", Pattern.MULTILINE);
private final Map<String, Translator<?>> translators;
@Inject
@@ -45,22 +71,32 @@ public class ASSSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
@Override
public String format(final SubtitleCollection<?> collection, final FormatOptions options) {
final var subtitles = collection.subtitles().stream().sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList();
final var scriptInfo = getScriptInfo(options.videoInfo());
final var styles = getStyles(subtitles, options.defaultFont());
final var events = getEvents(subtitles, options.defaultFont());
final var scriptInfo = writeScriptInfo(options.videoInfo());
final var styles = writeStyles(subtitles, options.defaultFont());
final var events = writeEvents(subtitles, options.defaultFont());
return scriptInfo + "\n\n" + styles + "\n\n" + events + "\n";
}
private static String getEvents(final Collection<? extends Subtitle> subtitles, final Font defaultFont) {
return EVENTS_SECTION + "\n" + "Format: Start, End, Style, Text\n" +
subtitles.stream().map(s -> getEvent(s, defaultFont)).collect(Collectors.joining("\n"));
private static String writeEvents(final Collection<? extends Subtitle> subtitles, final Font defaultFont) {
return EVENTS_SECTION + "\n" + "Format: " + String.join(", ", EVENT_FIELDS) + "\n" +
subtitles.stream().map(s -> writeEvent(s, defaultFont)).collect(Collectors.joining("\n"));
}
private static String getEvent(final Subtitle subtitle, final Font defaultFont) {
return DIALOGUE + " " + formatTime(subtitle.start()) + "," + formatTime(subtitle.end()) + "," + getName(subtitle.font(), defaultFont) + "," + subtitle.content();
private static String writeEvent(final Subtitle subtitle, final Font defaultFont) {
return DIALOGUE + " 0," +
formatTime(subtitle.start()) + "," +
formatTime(subtitle.end()) + "," +
getStyleName(subtitle.font(), defaultFont) + "," +
"X," +
"0000," +
"0000," +
"0000," +
"," +
(subtitle.bounds() == null ? "" : "{\\pos(" + (long) subtitle.bounds().x() + "," + (long) subtitle.bounds().y() + ")}") +
subtitle.content();
}
private static String getName(final Font font, final Font defaultFont) {
private static String getStyleName(final Font font, final Font defaultFont) {
final String fontName;
final int fontSize;
if (font == null) {
@@ -73,7 +109,7 @@ public class ASSSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
return fontName + fontSize;
}
private static String getStyles(final Collection<? extends Subtitle> subtitles, final Font defaultFont) {
private static String writeStyles(final Collection<? extends Subtitle> subtitles, final Font defaultFont) {
return STYLES_SECTION + "\n" + "Format: Name, Fontname, Fontsize\n" + listStyles(subtitles, defaultFont);
}
@@ -82,10 +118,10 @@ public class ASSSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
if (subtitles.stream().anyMatch(s -> s.font() == null)) {
uniqueStyles.add(defaultFont);
}
return uniqueStyles.stream().map(f -> STYLE + " " + getName(f, defaultFont) + ", " + f.name() + ", " + f.size()).collect(Collectors.joining("\n"));
return uniqueStyles.stream().map(f -> STYLE + " " + getStyleName(f, defaultFont) + ", " + f.name() + ", " + f.size()).collect(Collectors.joining("\n"));
}
private static String getScriptInfo(final VideoInfo videoInfo) {
private static String writeScriptInfo(final VideoInfo videoInfo) {
return """
[Script Info]
PlayResX: %d
@@ -95,72 +131,127 @@ public class ASSSubtitleConverter implements SubtitleConverter<SubtitleImpl> {
@Override
public SubtitleCollectionImpl<SubtitleImpl> parse(final String content, final ParseOptions options) throws ParseException {
final var fonts = parseFonts(content, options.defaultFont());
final var subtitles = parseSubtitles(content, fonts);
final var videoHeight = parseVideoHeight(content);
final var fonts = parseFonts(content);
final var subtitles = parseSubtitles(content, fonts, options.defaultFont(), videoHeight);
final var text = subtitles.stream().map(Subtitle::content).collect(Collectors.joining());
final var language = translators.values().iterator().next().getLanguage(text); //Use any translator for now
return new SubtitleCollectionImpl<>(text, subtitles, language);
}
private static List<SubtitleImpl> parseSubtitles(final String content, final Map<String, ? extends Font> fonts) throws ParseException {
final var fontIndex = content.indexOf(EVENTS_SECTION);
if (fontIndex == -1) {
private static int parseVideoHeight(final CharSequence content) {
final var matcher = PLAY_RES_Y_PATTERN.matcher(content);
if (matcher.find()) {
return Integer.parseInt(matcher.group("height"));
} else {
return -1;
}
}
private static List<SubtitleImpl> parseSubtitles(final String content, final Map<String, Font> fonts, final Font defaultFont, final int videoHeight) throws ParseException {
final var eventsIndex = content.indexOf(EVENTS_SECTION);
if (eventsIndex == -1) {
throw new ParseException("Events section not found in " + content);
} else {
final var split = NEWLINE_PATTERN.split(content.substring(fontIndex));
final List<String> fields;
if (split[0].startsWith(EVENTS_SECTION)) {
fields = getFields(split[1]);
} else {
throw new ParseException("Couldn't parse events : " + content);
final var split = NEWLINE_PATTERN.split(content.substring(eventsIndex));
final var fields = getFields(split[1]);
final var fieldsMapping = EVENT_FIELDS.stream().collect(Collectors.toMap(e -> e, fields::indexOf));
if (REQUIRED_EVENT_FIELDS.stream().anyMatch(s -> fieldsMapping.get(s) == -1)) {
throw new ParseException("Missing required fields : " + content);
}
final var startIndex = fields.indexOf("Start");
final var endIndex = fields.indexOf("End");
final var styleIndex = fields.indexOf("Style");
final var textIndex = fields.indexOf("Text");
if (startIndex == -1 || endIndex == -1 || styleIndex == -1 || textIndex == -1) {
throw new ParseException("Couldn't parse events : " + content);
if (fieldsMapping.get("Text") != fields.size() - 1) {
throw new ParseException("Text field should be the last field : " + content);
}
//Ignore effect for now
return Arrays.stream(split).filter(s -> s.startsWith(DIALOGUE))
.map(s -> {
final var values = Arrays.stream(s.replace(DIALOGUE, "").split(",")).map(String::trim).filter(w -> !w.isBlank()).toList();
final var start = parseTime(values.get(startIndex));
final var end = parseTime(values.get(endIndex));
final var style = values.get(styleIndex);
final var font = fonts.get(style);
final var text = values.stream().skip(textIndex).collect(Collectors.joining(", "));
return new SubtitleImpl(text, start, end, font, null); //TODO pos style overrides
final var values = parseValues(s, fields.size());
final var start = parseTime(values.get(fieldsMapping.get(START_FIELD)));
final var end = parseTime(values.get(fieldsMapping.get(END_FIELD)));
final var style = values.get(fieldsMapping.get(STYLE_FIELD));
final var marginBounds = getMarginBounds(values, fieldsMapping, videoHeight);
final var font = fonts.getOrDefault(style, defaultFont);
final var rawText = values.get(fieldsMapping.get(TEXT_FIELD));
final var matcher = POS_PATTERN.matcher(rawText);
if (matcher.find()) {
final var x = Integer.parseInt(matcher.group("x"));
final var y = Integer.parseInt(matcher.group("y"));
final var bounds = new BoundsImpl(x, y, 0, 0);
final var text = matcher.replaceAll("");
return new SubtitleImpl(text, start, end, font, bounds);
} else {
return new SubtitleImpl(rawText, start, end, font, marginBounds);
}
}).toList();
}
}
private static Map<String, Font> parseFonts(final String content, final Font defaultFont) throws ParseException {
final var fontIndex = content.indexOf(STYLES_SECTION);
if (fontIndex == -1) {
private static List<String> parseValues(final CharSequence value, final int size) {
final var trimmed = START_DIALOG_PATTERN.matcher(value).replaceAll("").trim();
final var values = new ArrayList<String>(EVENT_FIELDS.size());
var substring = trimmed;
while (!substring.isEmpty()) {
final var index = substring.indexOf(',');
if (index == -1 || values.size() == size - 1) {
values.add(substring);
substring = "";
} else {
values.add(substring.substring(0, index));
substring = substring.substring(index + 1);
}
}
return values;
}
private static Bounds getMarginBounds(final List<String> values, final Map<String, Integer> fieldsMapping, final int videoHeight) {
final var marginLIndex = fieldsMapping.get(MARGIN_L_FIELD);
final var marginRIndex = fieldsMapping.get(MARGIN_R_FIELD);
final var marginVIndex = fieldsMapping.get(MARGIN_V_FIELD);
if (marginLIndex == -1 || marginRIndex == -1 || marginVIndex == -1) {
return null;
} else {
final var marginL = parseMargin(values.get(marginLIndex));
final var marginR = parseMargin(values.get(marginRIndex));
final var marginV = parseMargin(values.get(marginVIndex));
if (marginL == -1 || marginR == -1 || marginV == -1) {
return null;
} else {
final var y = videoHeight <= 0 ? 0 : videoHeight - marginV;
return new BoundsImpl(marginL, y, marginR - (double) marginL, 0);
}
}
}
private static int parseMargin(final CharSequence margin) {
final var removedZero = START_ZERO_PATTERN.matcher(margin).replaceAll("");
return removedZero.isEmpty() ? -1 : Integer.parseInt(removedZero);
}
private static Map<String, Font> parseFonts(final String content) throws ParseException {
final var stylesIndex = content.indexOf(STYLES_SECTION);
if (stylesIndex == -1) {
throw new ParseException("Styles section not found in " + content);
} else {
final var split = NEWLINE_PATTERN.split(content.substring(fontIndex));
final List<String> fields;
if (split[0].startsWith(STYLES_SECTION)) {
fields = getFields(split[1]);
} else {
throw new ParseException("Couldn't parse styles : " + content);
}
final var fontNameIndex = fields.indexOf("Fontname");
final var fontSizeIndex = fields.indexOf("Fontsize");
if (fontNameIndex == -1 || fontSizeIndex == -1) {
throw new ParseException("Couldn't parse styles : " + content);
final var split = NEWLINE_PATTERN.split(content.substring(stylesIndex));
final var fields = getFields(split[1]);
final var fieldsMapping = STYLE_FIELDS.stream().collect(Collectors.toMap(e -> e, fields::indexOf));
if (REQUIRED_STYLE_FIELDS.stream().anyMatch(s -> fieldsMapping.get(s) == -1)) {
throw new ParseException("Missing required fields : " + content);
}
return Arrays.stream(split).filter(s -> s.startsWith(STYLE))
.map(s -> {
final var values = Arrays.stream(s.replace(STYLE, "").split(",")).map(String::trim).filter(w -> !w.isBlank()).toList();
final var name = values.get(fontNameIndex);
final var size = Integer.parseInt(values.get(fontSizeIndex));
return new FontImpl(name, size);
}).collect(Collectors.toMap(f -> getName(f, defaultFont), f -> f));
final var name = values.get(fieldsMapping.get(NAME_FIELD));
final var fontName = values.get(fieldsMapping.get(FONTNAME_FIELD));
final var fontSize = Integer.parseInt(values.get(fieldsMapping.get(FONTSIZE_FIELD)));
return new NamedFont(name, new FontImpl(fontName, fontSize));
}).collect(Collectors.toMap(f -> f.name, f -> f.font));
}
}
private record NamedFont(String name, Font font) {
}
private static List<String> getFields(final String string) {
return Arrays.stream(string.replace(FORMAT, "").split(",")).map(String::trim).filter(s -> !s.isBlank()).toList();
}

View File

@@ -6,6 +6,7 @@ import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.converter.FormatOptions;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
import com.github.gtache.autosubtitle.subtitle.converter.ParseOptions;
import com.github.gtache.autosubtitle.subtitle.impl.BoundsImpl;
import com.github.gtache.autosubtitle.subtitle.impl.FontImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl;
import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl;
@@ -13,10 +14,14 @@ import com.github.gtache.autosubtitle.translation.Translator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static java.util.Objects.requireNonNull;
@@ -56,6 +61,28 @@ class TestASSSubtitleConverter {
when(translator.getLanguage(anyString())).thenReturn(language);
}
@Test
void testFormatDefaultFont() {
final var font = new FontImpl("Times New Roman", 13);
when(formatOptions.defaultFont()).thenReturn(font);
final var subtitle = new SubtitleImpl("Test", 0, 1000, null, null);
final var expected = """
[Script Info]
PlayResX: 1920
PlayResY: 1080
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontname, Fontsize
Style: Times New Roman13, Times New Roman, 13
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,00:00:00.00,00:00:01.00,Times New Roman13,X,0000,0000,0000,,Test
""";
assertEquals(expected, converter.format(new SubtitleCollectionImpl<>(subtitle.content(), List.of(subtitle), language), formatOptions));
}
@Test
void testParseFormat() throws ParseException {
final var in = """
@@ -70,24 +97,119 @@ class TestASSSubtitleConverter {
Style: Arial12, Arial, 12
[Events]
Format: Start, End, Style, Text
Dialogue: 00:00:00.00,00:00:00.41,Arial12,Test ?
Dialogue: 123:45:54.32,124:00:00.00,Times New Roman13,Test2.
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,00:00:00.00,00:00:00.41,Arial12,X,0000,0000,0000,,Test ?
Dialogue: 0,00:00:00.00,00:00:00.54,Arial12,X,0000,0000,0000,,{\\pos(1000,880)}Testmargin
Dialogue: 0,00:00:00.00,00:00:11.00,Arial12,X,0000,0000,0000,,{\\pos(800,600)}Testpos
Dialogue: 0,123:45:54.32,124:00:00.00,Times New Roman13,X,0000,0000,0000,,Test2.
""";
final var start1 = 0L;
final var end1 = 410L;
final var start2 = 123 * 3600 * 1000 + 45 * 60 * 1000 + 54 * 1000 + 320;
final var end2 = 124 * 3600 * 1000;
final var end2 = 540L;
final var end3 = 11000L;
final var start4 = 123 * 3600 * 1000 + 45 * 60 * 1000 + 54 * 1000 + 320;
final var end4 = 124 * 3600 * 1000;
final var arial = new FontImpl("Arial", 12);
final var times = new FontImpl("Times New Roman", 13);
final var subtitle1 = new SubtitleImpl("Test ?", start1, end1, arial, null);
final var subtitle2 = new SubtitleImpl("Test2.", start2, end2, times, null);
final var subtitles = new SubtitleCollectionImpl<>(subtitle1.content() + subtitle2.content(), Arrays.asList(subtitle1, subtitle2), language);
final var subtitle2 = new SubtitleImpl("Testmargin", start1, end2, arial, new BoundsImpl(1000, 880, 0, 0));
final var subtitle3 = new SubtitleImpl("Testpos", start1, end3, arial, new BoundsImpl(800, 600, 0, 0));
final var subtitle4 = new SubtitleImpl("Test2.", start4, end4, times, null);
final var subtitles = new SubtitleCollectionImpl<>(subtitle1.content() + subtitle2.content()
+ subtitle3.content() + subtitle4.content(), List.of(subtitle1, subtitle2, subtitle3, subtitle4), language);
assertEquals(subtitles, converter.parse(in, parseOptions));
assertEquals(in, converter.format(subtitles, formatOptions));
}
@Test
void testParseMargin() throws ParseException {
final var in = """
[Script Info]
PlayResX: 1920
PlayResY: 1080
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontname, Fontsize
Style: Arial12, Arial, 12
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,00:00:00.00,00:00:00.54,Arial12,X,1000,1500,0200,,Testmargin
""";
final var start = 0L;
final var end = 540L;
final var arial = new FontImpl("Arial", 12);
final var subtitle = new SubtitleImpl("Testmargin", start, end, arial, new BoundsImpl(1000, 880, 500, 0));
final var subtitles = new SubtitleCollectionImpl<>(subtitle.content(), List.of(subtitle), language);
assertEquals(subtitles, converter.parse(in, parseOptions));
final var out = """
[Script Info]
PlayResX: 1920
PlayResY: 1080
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontname, Fontsize
Style: Arial12, Arial, 12
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,00:00:00.00,00:00:00.54,Arial12,X,0000,0000,0000,,{\\pos(1000,880)}Testmargin
""";
assertEquals(out, converter.format(subtitles, formatOptions));
}
@Test
void testParseMarginNoSize() throws ParseException {
final var in = """
[Script Info]
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontname, Fontsize
Style: Arial12, Arial, 12
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,00:00:00.00,00:00:00.54,Arial12,X,1000,1500,0200,,Testmargin
""";
final var start = 0L;
final var end = 540L;
final var arial = new FontImpl("Arial", 12);
final var subtitle = new SubtitleImpl("Testmargin", start, end, arial, new BoundsImpl(1000, 0, 500, 0));
final var subtitles = new SubtitleCollectionImpl<>(subtitle.content(), List.of(subtitle), language);
assertEquals(subtitles, converter.parse(in, parseOptions));
}
@Test
void testParseUnknownFont() throws ParseException {
final var defaultFont = new FontImpl("Times New Roman", 13);
when(parseOptions.defaultFont()).thenReturn(defaultFont);
final var in = """
[Script Info]
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontname, Fontsize
Style: Arial12, Arial, 12
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,00:00:00.00,00:00:00.54,blabla,X,1000,1500,0200,,Testmargin
""";
final var start = 0L;
final var end = 540L;
final var subtitle = new SubtitleImpl("Testmargin", start, end, defaultFont, new BoundsImpl(1000, 0, 500, 0));
final var subtitles = new SubtitleCollectionImpl<>(subtitle.content(), List.of(subtitle), language);
assertEquals(subtitles, converter.parse(in, parseOptions));
}
@Test
void testParseDifferentFormat() throws ParseException {
final var in = """
@@ -119,6 +241,110 @@ class TestASSSubtitleConverter {
assertEquals(subtitles, converter.parse(in, parseOptions));
}
@Test
void testParseNoEvents() {
final var in = """
[Script Info]
PlayResX: 1920
PlayResY: 1080
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontsize, Fontname
Style: Arial12, 12, Arial
""";
assertThrows(ParseException.class, () -> converter.parse(in, parseOptions));
}
@Test
void testParseNoStyles() {
final var in = """
[Script Info]
PlayResX: 1920
PlayResY: 1080
WrapStyle: 1
[Events]
Format: Style, End, Start, Text
Dialogue: Arial12,0:00:00.41,0:00:00.00,Test ?
Dialogue: Times New Roman13,124:00:00.00,123:45:54.32,Test2.
""";
assertThrows(ParseException.class, () -> converter.parse(in, parseOptions));
}
@ParameterizedTest
@ValueSource(strings = {"Name", "Fontsize", "Fontname"})
void testParseMissingRequiredStyleFields(final String missing) {
final var present = new ArrayList<String>();
present.add("Name");
present.add("Fontname");
present.add("Fontsize");
present.remove(missing);
final var in = """
[Script Info]
PlayResX: 1920
PlayResY: 1080
WrapStyle: 1
[V4+ Styles]
Format: %s
Style: Arial12, 12, Arial
[Events]
Format: Start, End, Text, Style
Dialogue: Arial12,0:00:00.41,0:00:00.00,Test ?
Dialogue: Times New Roman13,124:00:00.00,123:45:54.32,Test2.
""".formatted(String.join(", ", present));
assertThrows(ParseException.class, () -> converter.parse(in, parseOptions));
}
@ParameterizedTest
@ValueSource(strings = {"Start", "End", "Style", "Text"})
void testParseMissingRequiredEventFields(final String missing) {
final var present = new ArrayList<String>();
present.add("Start");
present.add("End");
present.add("Style");
present.add("Text");
present.remove(missing);
final var in = """
[Script Info]
PlayResX: 1920
PlayResY: 1080
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontsize, Fontname
Style: Arial12, 12, Arial
[Events]
Format: %s
Dialogue: Arial12,0:00:00.41,0:00:00.00,Test ?
Dialogue: Times New Roman13,124:00:00.00,123:45:54.32,Test2.
""".formatted(String.join(", ", present));
assertThrows(ParseException.class, () -> converter.parse(in, parseOptions));
}
@Test
void testParseTextNotLast() {
final var in = """
[Script Info]
PlayResX: 1920
PlayResY: 1080
WrapStyle: 1
[V4+ Styles]
Format: Name, Fontsize, Fontname
Style: Arial12, 12, Arial
[Events]
Format: Start, End, Text, Style
Dialogue: Arial12,0:00:00.41,0:00:00.00,Test ?
Dialogue: Times New Roman13,124:00:00.00,123:45:54.32,Test2.
""";
assertThrows(ParseException.class, () -> converter.parse(in, parseOptions));
}
@Test
void testParseException() {
final var in = """