From 2fcb8cabc946ff7f06bfdfc44eefa528671c3a89 Mon Sep 17 00:00:00 2001 From: niksedk Date: Sun, 23 Aug 2026 10:30:56 +0200 Subject: [PATCH] Fix ten more bugs from a second random sweep The four "DVD Studio Pro with space" readers cut fixed offsets out of a line whose regex declares variable-width time codes, so a valid file threw ArgumentOutOfRangeException; they now split on the separator, which also keeps a separator that appears inside the text. Csv had the same shape: it split on every ";" and required exactly four parts, so any line whose text contained a semicolon was dropped without an error - even one Csv itself had written. AssaResampler cut tag parameters at a fixed offset although its regexes allow whitespace, so "\pos (10,11)" failed to parse and the "break" then abandoned every remaining tag on the line. Paragraph.WordsPerMinute had no zero-duration guard, unlike GetCharactersPerSecond right below it, so one zero-length line made the average and maximum WPM of a whole statistics report infinite. WebVTT thumbnails rejected every .jpeg sprite sheet, and the .jpeg branch of GetBitmap indexed with posJpg instead of posJpeg. Netflix "spell out leading number" matched only "\r\n" so the rule was dead off Windows; it now captures the digits and accepts either line break. Netflix max-line-length logged the same fix once per over-long line. Video OCR dropped unreadable frames out of their group instead of extending it, shortening the subtitle. ToRawAss had an unreachable duplicate "strikeout" branch, and ASSA CheckForErrors cut the untrimmed line after testing the trimmed one. Co-Authored-By: Claude Opus 5 --- src/libse/Common/AssaResampler.cs | 32 +++-- src/libse/Common/Paragraph.cs | 8 ++ src/libse/Common/SsaStyle.cs | 4 - .../AdvancedSubStationAlpha.cs | 5 +- src/libse/SubtitleFormats/Csv.cs | 4 +- .../SubtitleFormats/DvdStudioProSpace.cs | 9 +- .../DvdStudioProSpaceGraphic.cs | 9 +- .../SubtitleFormats/DvdStudioProSpaceOne.cs | 9 +- .../DvdStudioProSpaceOneSemicolon.cs | 9 +- src/libse/SubtitleFormats/WebVttThumbnail.cs | 12 +- .../Video/VideoOcr/VideoOcrFrameGrouper.cs | 10 +- .../NetflixCheckMaxLineLength.cs | 14 ++- .../NetflixCheckStartNumberSpellOut.cs | 49 ++++---- tests/libse/Core/BugHunt20260823Round2Test.cs | 111 ++++++++++++++++++ 14 files changed, 230 insertions(+), 55 deletions(-) create mode 100644 tests/libse/Core/BugHunt20260823Round2Test.cs 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)); + } +}