diff --git a/src/libse/Common/AssaResampler.cs b/src/libse/Common/AssaResampler.cs index 92b5c869930..84c3d969f02 100644 --- a/src/libse/Common/AssaResampler.cs +++ b/src/libse/Common/AssaResampler.cs @@ -183,6 +183,20 @@ private static string FixDrawing(decimal sourceWidth, decimal targetWidth, decim return sb.ToString().TrimEnd() + s; } + // The tag regexes below allow whitespace ("\pos (10,11)"), so the parameters cannot be cut + // at a fixed offset from the tag name - take everything between the parentheses instead. + private static string GetTagParameters(string matchValue) + { + var open = matchValue.IndexOf('('); + var close = matchValue.LastIndexOf(')'); + if (open < 0 || close <= open) + { + return string.Empty; + } + + return matchValue.Substring(open + 1, close - open - 1).RemoveChar(' '); + } + private static string FixMethodFourParameters(decimal sourceWidth, decimal targetWidth, decimal sourceHeight, decimal targetHeight, string input, string tag) { var regex = GetCachedRegex("\\\\" + tag + "\\s*\\(\\s*[-+]?\\d+[\\.\\d+]*\\s*,\\s*[-+]?\\d+[\\.\\d+]*\\s*,\\s*[-+]?\\d+[\\.\\d+]*\\s*,\\s*[-+]?\\d+[\\.\\d+]*\\s*\\)"); @@ -190,7 +204,7 @@ private static string FixMethodFourParameters(decimal sourceWidth, decimal targe var match = regex.Match(s); while (match.Success) { - var value = match.Value.Substring(tag.Length + 2, match.Value.Length - tag.Length - 3).RemoveChar(' '); + var value = GetTagParameters(match.Value); var arr = value.Split(','); if (arr.Length == 4 && decimal.TryParse(arr[0], NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var x1) && @@ -212,7 +226,8 @@ private static string FixMethodFourParameters(decimal sourceWidth, decimal targe } else { - break; + // Skip this tag rather than abandoning every remaining tag on the line. + match = regex.Match(s, match.Index + match.Value.Length); } } @@ -226,7 +241,7 @@ private static string FixMethodSixParametersFourActive(decimal sourceWidth, deci var match = regex.Match(s); while (match.Success) { - var value = match.Value.Substring(tag.Length + 2, match.Value.Length - tag.Length - 3).RemoveChar(' '); + var value = GetTagParameters(match.Value); var arr = value.Split(','); if (arr.Length == 6 && decimal.TryParse(arr[0], NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var x1) && @@ -252,7 +267,8 @@ private static string FixMethodSixParametersFourActive(decimal sourceWidth, deci } else { - break; + // Skip this tag rather than abandoning every remaining tag on the line. + match = regex.Match(s, match.Index + match.Value.Length); } } @@ -266,7 +282,7 @@ private static string FixMethodTwoParameters(decimal sourceWidth, decimal target var match = regex.Match(s); while (match.Success) { - var value = match.Value.Substring(tag.Length + 2, match.Value.Length - tag.Length - 3).RemoveChar(' '); + var value = GetTagParameters(match.Value); var arr = value.Split(','); if (arr.Length == 2 && decimal.TryParse(arr[0], NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var x) && @@ -282,7 +298,8 @@ private static string FixMethodTwoParameters(decimal sourceWidth, decimal target } else { - break; + // Skip this tag rather than abandoning every remaining tag on the line. + match = regex.Match(s, match.Index + match.Value.Length); } } @@ -306,7 +323,8 @@ private static string FixTagWithNumber(decimal sourceHeight, decimal targetHeigh } else { - break; + // Skip this tag rather than abandoning every remaining tag on the line. + match = regex.Match(s, match.Index + match.Value.Length); } } diff --git a/src/libse/Common/Paragraph.cs b/src/libse/Common/Paragraph.cs index d251bd8740b..4823d101e98 100644 --- a/src/libse/Common/Paragraph.cs +++ b/src/libse/Common/Paragraph.cs @@ -131,6 +131,14 @@ public double WordsPerMinute return 0; } + // Same guard as GetCharactersPerSecond: a zero or negative duration would give + // infinity/a negative rate, which poisons the min/max/average of every statistics + // report that sums this up. + if (DurationTotalMilliseconds < 1) + { + return 999; + } + return 60.0 / DurationTotalSeconds * Text.CountWords(); } } diff --git a/src/libse/Common/SsaStyle.cs b/src/libse/Common/SsaStyle.cs index 77eecc7ff28..836d67f9c60 100644 --- a/src/libse/Common/SsaStyle.cs +++ b/src/libse/Common/SsaStyle.cs @@ -291,10 +291,6 @@ public string ToRawAss(string styleFormat = DefaultAssStyleFormat) { sb.Append('1'); } - else if (f == "strikeout") - { - sb.Append('0'); - } else if (f == "scalex") { sb.Append(ScaleX.ToString(CultureInfo.InvariantCulture)); diff --git a/src/libse/SubtitleFormats/AdvancedSubStationAlpha.cs b/src/libse/SubtitleFormats/AdvancedSubStationAlpha.cs index 6d65457889c..cb915b5d46e 100644 --- a/src/libse/SubtitleFormats/AdvancedSubStationAlpha.cs +++ b/src/libse/SubtitleFormats/AdvancedSubStationAlpha.cs @@ -2302,7 +2302,8 @@ public static string CheckForErrors(string header) { if (line.Length > 10) { - var format = line.Substring(8).ToLowerInvariant().Split(','); + // cut the trimmed line - leading whitespace shifted every field name + var format = s.Substring(8).Split(','); styleCount = format.Length; for (int i = 0; i < format.Length; i++) { @@ -2403,7 +2404,7 @@ public static string CheckForErrors(string header) if (line.Length > 10) { string rawLine = line; - var format = line.Substring(6).Split(','); + var format = line.Trim().Substring(6).Split(','); if (format.Length != styleCount) { diff --git a/src/libse/SubtitleFormats/Csv.cs b/src/libse/SubtitleFormats/Csv.cs index 6504258ef00..83202d3530b 100644 --- a/src/libse/SubtitleFormats/Csv.cs +++ b/src/libse/SubtitleFormats/Csv.cs @@ -54,7 +54,9 @@ public override void LoadSubtitle(Subtitle subtitle, List lines, string { if (CsvLine.IsMatch(line)) { - var parts = line.Split(Separator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); + // Only split off the three leading numeric fields - the text is the rest of the + // line and may contain the separator itself (and may be empty). + var parts = line.Split(Separator.ToCharArray(), 4, StringSplitOptions.None); if (parts.Length == 4) { try diff --git a/src/libse/SubtitleFormats/DvdStudioProSpace.cs b/src/libse/SubtitleFormats/DvdStudioProSpace.cs index 10eefefa0c9..53151f9e696 100644 --- a/src/libse/SubtitleFormats/DvdStudioProSpace.cs +++ b/src/libse/SubtitleFormats/DvdStudioProSpace.cs @@ -50,15 +50,18 @@ public override void LoadSubtitle(Subtitle subtitle, List lines, string { if (RegexTimeCodes.IsMatch(line)) { - string[] toPart = line.Substring(0, 25).Split(new[] { " ," }, StringSplitOptions.None); + // Split on the separator instead of fixed offsets - the time code parts are + // "\d+" in the regex above, so they are not always two digits wide, and the + // text may itself contain " , ". + string[] toPart = line.Split(new[] { " , " }, 3, StringSplitOptions.None); Paragraph p = new Paragraph(); - if (toPart.Length == 2 && + if (toPart.Length == 3 && DvdStudioPro.GetTimeCode(p.StartTime, toPart[0]) && DvdStudioPro.GetTimeCode(p.EndTime, toPart[1])) { number++; p.Number = number; - string text = line.Substring(27).Trim(); + string text = toPart[2].Trim(); p.Text = text.Replace(" | ", Environment.NewLine).Replace("|", Environment.NewLine); p.Text = DvdStudioPro.DecodeStyles(p.Text); if (p.Text.Trim().StartsWith("<>")) diff --git a/src/libse/SubtitleFormats/DvdStudioProSpaceGraphic.cs b/src/libse/SubtitleFormats/DvdStudioProSpaceGraphic.cs index e41f942cd8e..f13fe2654f2 100644 --- a/src/libse/SubtitleFormats/DvdStudioProSpaceGraphic.cs +++ b/src/libse/SubtitleFormats/DvdStudioProSpaceGraphic.cs @@ -46,15 +46,18 @@ public override void LoadSubtitle(Subtitle subtitle, List lines, string { if (RegexTimeCodes.IsMatch(line)) { - string[] toPart = line.Substring(0, 25).Split(new[] { " ," }, StringSplitOptions.None); + // Split on the separator instead of fixed offsets - the time code parts are + // "\d+" in the regex above, so they are not always two digits wide, and the + // text may itself contain " , ". + string[] toPart = line.Split(new[] { " , " }, 3, StringSplitOptions.None); Paragraph p = new Paragraph(); - if (toPart.Length == 2 && + if (toPart.Length == 3 && DvdStudioPro.GetTimeCode(p.StartTime, toPart[0]) && DvdStudioPro.GetTimeCode(p.EndTime, toPart[1])) { number++; p.Number = number; - string text = line.Substring(27).Trim(); + string text = toPart[2].Trim(); p.Text = text.Replace(" | ", Environment.NewLine).Replace("|", Environment.NewLine); p.Text = DvdStudioPro.DecodeStyles(p.Text); p.Text = DvdStudioPro.GetAlignment(verticalAlign, horizontalAlign) + p.Text; diff --git a/src/libse/SubtitleFormats/DvdStudioProSpaceOne.cs b/src/libse/SubtitleFormats/DvdStudioProSpaceOne.cs index c559882ce34..77dc5d56bde 100644 --- a/src/libse/SubtitleFormats/DvdStudioProSpaceOne.cs +++ b/src/libse/SubtitleFormats/DvdStudioProSpaceOne.cs @@ -54,15 +54,18 @@ public override void LoadSubtitle(Subtitle subtitle, List lines, string { if (RegexTimeCodes.IsMatch(line)) { - string[] toPart = line.Substring(0, 24).Trim(',').Split(','); + // Split on the separator instead of fixed offsets - the time code parts are + // "\d+" in the regex above, so they are not always two digits wide, and the + // text may itself contain ",". + string[] toPart = line.Split(new[] { ',' }, 3); var p = new Paragraph(); - if (toPart.Length == 2 && + if (toPart.Length == 3 && DvdStudioPro.GetTimeCode(p.StartTime, toPart[0]) && DvdStudioPro.GetTimeCode(p.EndTime, toPart[1])) { number++; p.Number = number; - string text = line.Substring(25).Trim(); + string text = toPart[2].Trim(); p.Text = text.Replace(" | ", Environment.NewLine).Replace("|", Environment.NewLine); p.Text = DvdStudioPro.DecodeStyles(p.Text); if (italicOn && !p.Text.Contains("")) diff --git a/src/libse/SubtitleFormats/DvdStudioProSpaceOneSemicolon.cs b/src/libse/SubtitleFormats/DvdStudioProSpaceOneSemicolon.cs index d6bb84b7403..a758632119b 100644 --- a/src/libse/SubtitleFormats/DvdStudioProSpaceOneSemicolon.cs +++ b/src/libse/SubtitleFormats/DvdStudioProSpaceOneSemicolon.cs @@ -52,15 +52,18 @@ public override void LoadSubtitle(Subtitle subtitle, List lines, string { if (RegexTimeCodes.IsMatch(line)) { - string[] toPart = line.Substring(0, 24).Trim(',').Split(','); + // Split on the separator instead of fixed offsets - the time code parts are + // "\d+" in the regex above, so they are not always two digits wide, and the + // text may itself contain ",". + string[] toPart = line.Split(new[] { ',' }, 3); var p = new Paragraph(); - if (toPart.Length == 2 && + if (toPart.Length == 3 && DvdStudioPro.GetTimeCode(p.StartTime, toPart[0]) && DvdStudioPro.GetTimeCode(p.EndTime, toPart[1])) { number++; p.Number = number; - string text = line.Substring(25).Trim(); + string text = toPart[2].Trim(); p.Text = text.Replace(" | ", Environment.NewLine).Replace("|", Environment.NewLine); p.Text = DvdStudioPro.DecodeStyles(p.Text); if (italicOn && !p.Text.Contains("")) diff --git a/src/libse/SubtitleFormats/WebVttThumbnail.cs b/src/libse/SubtitleFormats/WebVttThumbnail.cs index 4f3fecc70fa..2f925f50de9 100644 --- a/src/libse/SubtitleFormats/WebVttThumbnail.cs +++ b/src/libse/SubtitleFormats/WebVttThumbnail.cs @@ -47,6 +47,11 @@ public override void LoadSubtitle(Subtitle subtitle, List lines, string continue; } + if (p.Text.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + if (p.Text.Contains(".png#xywh=", StringComparison.OrdinalIgnoreCase)) { continue; @@ -57,6 +62,11 @@ public override void LoadSubtitle(Subtitle subtitle, List lines, string continue; } + if (p.Text.Contains(".jpeg#xywh=", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + return; } @@ -108,7 +118,7 @@ public SKBitmap GetBitmap(string fileName, Subtitle subtitle, int index) var posJpeg = imageFileName.IndexOf(".jpeg#xywh=", StringComparison.OrdinalIgnoreCase); if (posJpeg >= 0) { - string spec = imageFileName.Substring(posJpg + 11); // after ".jepg#xywh=" + string spec = imageFileName.Substring(posJpeg + 11); // after ".jpeg#xywh=" imageFileName = imageFileName.Substring(0, posJpeg + 5); // keep ".jpeg" ParseSpriteSpec(spec, out x, out y, out w, out h); useSprite = true; diff --git a/src/ui/Features/Video/VideoOcr/VideoOcrFrameGrouper.cs b/src/ui/Features/Video/VideoOcr/VideoOcrFrameGrouper.cs index e2d6d203d66..c0d6dffd556 100644 --- a/src/ui/Features/Video/VideoOcr/VideoOcrFrameGrouper.cs +++ b/src/ui/Features/Video/VideoOcr/VideoOcrFrameGrouper.cs @@ -39,7 +39,15 @@ public static List Group( var thumbnail = MakeThumbnail(frameFileNames[index], brightnessMinimum); if (thumbnail == null) { - continue; // unreadable frame - treat as part of the current group + // Unreadable frame - keep it inside the current group. Skipping it outright left + // the group's EndFrame behind, which shortens the subtitle's end time. + if (current != null) + { + current.EndFrame = index; + currentFileList.Add(frameFileNames[index]); + } + + continue; } var isBlank = brightnessMinimum > 0 && IsBlank(thumbnail); diff --git a/src/ui/Logic/NetflixQualityCheck/NetflixCheckMaxLineLength.cs b/src/ui/Logic/NetflixQualityCheck/NetflixCheckMaxLineLength.cs index 612acf55e1d..610288668d5 100644 --- a/src/ui/Logic/NetflixQualityCheck/NetflixCheckMaxLineLength.cs +++ b/src/ui/Logic/NetflixQualityCheck/NetflixCheckMaxLineLength.cs @@ -23,8 +23,16 @@ public void Check(Subtitle subtitle, NetflixQualityController controller) { foreach (var p in subtitle.Paragraphs) { + // One record per paragraph - the fix below re-breaks the whole paragraph, so a + // second over-long line would add a duplicate record with the identical fix. + var reported = false; foreach (var line in p.Text.SplitToLines()) { + if (reported) + { + break; + } + if (controller.Language == "ja") { var vertical = p.Text.Contains("{\\an7", StringComparison.Ordinal) || p.Text.Contains("{\\an9", StringComparison.Ordinal); @@ -36,6 +44,7 @@ public void Check(Subtitle subtitle, NetflixQualityController controller) { var comment = Se.Language.Tools.NetflixCheckAndFix.SingleVerticalLineLengthMax11; controller.AddRecord(p, p.StartTime.ToHHMMSSFF(), line.Length.ToString(CultureInfo.InvariantCulture), comment, false); + reported = true; } } else // Horizontal subtitles - Maximum 13 full-width characters per line @@ -44,6 +53,7 @@ public void Check(Subtitle subtitle, NetflixQualityController controller) { var comment = Se.Language.Tools.NetflixCheckAndFix.SingleHorizontalLineLengthMax13; controller.AddRecord(p, p.StartTime.ToHHMMSSFF(), line.Length.ToString(CultureInfo.InvariantCulture), comment); + reported = true; } } } @@ -53,13 +63,15 @@ public void Check(Subtitle subtitle, NetflixQualityController controller) fixedParagraph.Text = Utilities.AutoBreakLine(fixedParagraph.Text, controller.SingleLineMaxLength, controller.SingleLineMaxLength - 3, controller.Language); var comment = string.Format(Se.Language.Tools.NetflixCheckAndFix.SingleLineLengthExceedsX, controller.SingleLineMaxLength); controller.AddRecord(p, fixedParagraph, comment, line.CountCharacters(nameof(CalcCjk), false).ToString(CultureInfo.InvariantCulture), true); + reported = true; } else if (line.CountCharacters(false) > controller.SingleLineMaxLength) { var fixedParagraph = new Paragraph(p, false); fixedParagraph.Text = Utilities.AutoBreakLine(fixedParagraph.Text, controller.SingleLineMaxLength, controller.SingleLineMaxLength - 3, controller.Language); var comment = string.Format(Se.Language.Tools.NetflixCheckAndFix.SingleLineLengthExceedsX, controller.SingleLineMaxLength); - controller.AddRecord(p, fixedParagraph, comment, line.Length.ToString(CultureInfo.InvariantCulture), true ); + controller.AddRecord(p, fixedParagraph, comment, line.Length.ToString(CultureInfo.InvariantCulture), true); + reported = true; } } } diff --git a/src/ui/Logic/NetflixQualityCheck/NetflixCheckStartNumberSpellOut.cs b/src/ui/Logic/NetflixQualityCheck/NetflixCheckStartNumberSpellOut.cs index 1328e30d31f..d924235609f 100644 --- a/src/ui/Logic/NetflixQualityCheck/NetflixCheckStartNumberSpellOut.cs +++ b/src/ui/Logic/NetflixQualityCheck/NetflixCheckStartNumberSpellOut.cs @@ -9,9 +9,13 @@ namespace Nikse.SubtitleEdit.Logic.NetflixQualityCheck; /// public class NetflixCheckStartNumberSpellOut : INetflixQualityChecker { - private static readonly Regex NumberStart = new Regex(@"^\d+ [A-Za-z]", RegexOptions.Compiled); - private static readonly Regex NumberStartInside = new Regex(@"[\.,!] \d+ [A-Za-z]", RegexOptions.Compiled); - private static readonly Regex NumberStartInside2 = new Regex(@"[\.,!]\r\n\d+ [A-Za-z]", RegexOptions.Compiled); + // The digits are captured so the replacement can use the group's own index/length - the + // offsets used to be counted off the whole match, which broke as soon as the separator was + // not exactly the assumed width. The line break alternation matters off Windows, where + // paragraph text is separated by "\n" and the old "\r\n" pattern never matched at all. + private static readonly Regex NumberStart = new Regex(@"^(\d+) [A-Za-z]", RegexOptions.Compiled); + private static readonly Regex NumberStartInside = new Regex(@"[\.,!] (\d+) [A-Za-z]", RegexOptions.Compiled); + private static readonly Regex NumberStartInside2 = new Regex(@"[\.,!](?:\r\n|\n|\r)(\d+) [A-Za-z]", RegexOptions.Compiled); public string Name { get; set; } @@ -26,29 +30,9 @@ public void Check(Subtitle subtitle, NetflixQualityController controller) { var newText = p.Text; - var m = NumberStart.Match(newText); - while (m.Success) - { - var length = m.Length - 2; - newText = newText.Remove(m.Index, length).Insert(m.Index, NetflixHelper.ConvertNumberToString(m.Value.Substring(0, length), true, controller.Language)); - m = NumberStart.Match(newText, m.Index + 1); - } - - m = NumberStartInside.Match(newText); - while (m.Success) - { - var length = m.Length - 4; - newText = newText.Remove(m.Index + 2, length).Insert(m.Index + 2, NetflixHelper.ConvertNumberToString(m.Value.Substring(2, length), true, controller.Language)); - m = NumberStartInside.Match(newText, m.Index + 1); - } - - m = NumberStartInside2.Match(newText); - while (m.Success) - { - var length = m.Length - 5; - newText = newText.Remove(m.Index + 3, length).Insert(m.Index + 3, NetflixHelper.ConvertNumberToString(m.Value.Substring(3, length), true, controller.Language)); - m = NumberStartInside2.Match(newText, m.Index + 1); - } + newText = SpellOutNumbers(NumberStart, newText, controller.Language); + newText = SpellOutNumbers(NumberStartInside, newText, controller.Language); + newText = SpellOutNumbers(NumberStartInside2, newText, controller.Language); if (newText != p.Text) { @@ -59,4 +43,17 @@ public void Check(Subtitle subtitle, NetflixQualityController controller) } } + private static string SpellOutNumbers(Regex regex, string text, string language) + { + var m = regex.Match(text); + while (m.Success) + { + var digits = m.Groups[1]; + text = text.Remove(digits.Index, digits.Length) + .Insert(digits.Index, NetflixHelper.ConvertNumberToString(digits.Value, true, language)); + m = regex.Match(text, m.Index + 1); + } + + return text; + } } diff --git a/tests/libse/Core/BugHunt20260823Round2Test.cs b/tests/libse/Core/BugHunt20260823Round2Test.cs new file mode 100644 index 00000000000..871de93167a --- /dev/null +++ b/tests/libse/Core/BugHunt20260823Round2Test.cs @@ -0,0 +1,111 @@ +using Nikse.SubtitleEdit.Core.Common; +using Nikse.SubtitleEdit.Core.SubtitleFormats; + +namespace LibSETests.Core; + +public class BugHunt20260823Round2Test +{ + [Fact] + public void DvdStudioProSpace_ShortTimeCodesAndSeparatorInText() + { + var sub = new Subtitle(); + new DvdStudioProSpace().LoadSubtitle(sub, new List { "0:0:0:0 , 0:0:0:1 , Hi , there" }, null); + Assert.Single(sub.Paragraphs); + Assert.Equal("Hi , there", sub.Paragraphs[0].Text); + Assert.Equal(0, sub.Paragraphs[0].StartTime.TotalMilliseconds); + } + + [Fact] + public void DvdStudioProSpaceGraphic_ShortTimeCodes() + { + var sub = new Subtitle(); + new DvdStudioProSpaceGraphic().LoadSubtitle(sub, new List { "0:0:0:0 , 0:0:0:1 , <>a.png" }, null); + Assert.Single(sub.Paragraphs); + Assert.EndsWith("a.png", sub.Paragraphs[0].Text); + Assert.Equal(0, sub.Paragraphs[0].StartTime.TotalMilliseconds); + } + + [Fact] + public void DvdStudioProSpaceOne_ShortTimeCodes() + { + var sub = new Subtitle(); + new DvdStudioProSpaceOne().LoadSubtitle(sub, new List { "0:0:0:0,0:0:0:1, Hi, there" }, null); + Assert.Single(sub.Paragraphs); + Assert.Equal("Hi, there", sub.Paragraphs[0].Text); + } + + [Fact] + public void DvdStudioProSpaceOneSemicolon_ShortTimeCodes() + { + new DvdStudioProSpaceOneSemicolon().LoadSubtitle(new Subtitle(), new List { "0:0:0;0,0:0:0;1, Hi" }, null); + } + + [Fact] + public void Csv_RoundTripWithSeparatorInText() + { + var sub = new Subtitle(); + sub.Paragraphs.Add(new Paragraph("Hello; world", 0, 1000)); + sub.Paragraphs.Add(new Paragraph("Plain line", 2000, 3000)); + + var loaded = new Subtitle(); + new Csv().LoadSubtitle(loaded, new Csv().ToText(sub, "t").SplitToLines(), null); + + Assert.Equal(2, loaded.Paragraphs.Count); + Assert.Equal("Hello; world", loaded.Paragraphs[0].Text); + Assert.Equal("Plain line", loaded.Paragraphs[1].Text); + } + + [Fact] + public void AssaResampler_WhitespaceBeforeParenthesis() + { + Assert.Equal("{\\pos(20,22)}Hi", AssaResampler.ResampleOverrideTagsPosition(720, 1440, 480, 960, "{\\pos(10,11)}Hi")); + Assert.Equal("{\\pos(20,22)}Hi", AssaResampler.ResampleOverrideTagsPosition(720, 1440, 480, 960, "{\\pos (10,11)}Hi")); + Assert.Equal("{\\move(20,22,40,42)}Hi", AssaResampler.ResampleOverrideTagsPosition(720, 1440, 480, 960, "{\\move ( 10 , 11 , 20 , 21 )}Hi")); + } + + [Fact] + public void AssaResampler_OneOddTagDoesNotAbandonTheRest() + { + var result = AssaResampler.ResampleOverrideTagsPosition(720, 1440, 480, 960, "{\\pos (10,11)}A{\\pos(30,31)}B"); + Assert.Equal("{\\pos(20,22)}A{\\pos(60,62)}B", result); + } + + [Fact] + public void WordsPerMinute_IsFiniteForZeroAndNegativeDuration() + { + Assert.True(double.IsFinite(new Paragraph("Hello world", 1000, 1000).WordsPerMinute)); + Assert.True(new Paragraph("Hello world", 2000, 1000).WordsPerMinute >= 0); + // unchanged for a normal line: 2 words in 1 second = 120 wpm + Assert.Equal(120, new Paragraph("Hello world", 0, 1000).WordsPerMinute, 3); + } + + [Fact] + public void WebVttThumbnail_AcceptsJpeg() + { + var sub = new Subtitle(); + new WebVttThumbnail().LoadSubtitle(sub, new List + { + "WEBVTT", "", "00:00:00.000 --> 00:00:10.000", "sheet.jpeg#xywh=0,0,120,67", "" + }, null); + Assert.Single(sub.Paragraphs); + } + + [Fact] + public void SsaStyle_StrikeoutIsWrittenFromTheStyle() + { + const string format = "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding"; + var style = new SsaStyle { Name = "Test", Strikeout = true }; + var raw = style.ToRawAss(format); + // StrikeOut is field 11 (0-based 10) - it must carry the style's own value, not a constant + Assert.Equal("-1", raw.Substring("Style: ".Length).Split(',')[10].Trim()); + } + + [Fact] + public void AssaCheckForErrors_IndentedHeaderStillFindsEmptyName() + { + const string header = @"[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: ,Arial,20,&H00FFFFFF,&H0300FFFF,&H00000000,&H02000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1"; + Assert.Contains("'Name' is empty", AdvancedSubStationAlpha.CheckForErrors(header)); + } +}