mirror of
https://github.com/imrayya/SRTto3Dsubtitles.git
synced 2026-09-21 10:17:27 +02:00
feat: add CLI options and support SBS/OU 3D subtitle modes
- add named CLI arguments for mode, resolution, font size, and output path - support stereo modes: sbs (side-by-side) and ou (over-under) - write output .ass files next to the input file by default - add console logging, usage help, and argument validation - update ASS style generation for SBS and OU layouts - rename OU styles to Top and Bottom - fix HTML font color conversion to proper ASS color ordering - refactor runtime settings into an Options class Side Note: As a hearing impared person, I love this project and am glad it exists and I dont have to start from scratch.
This commit is contained in:
+364
-105
@@ -5,41 +5,227 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
//SSA/ASS specification v4+ http://www.tcax.org/docs/ass-specs.htm
|
// SSA/ASS specification v4+ http://www.tcax.org/docs/ass-specs.htm
|
||||||
//SRT (SubRip) specification https://en.wikipedia.org/wiki/SubRip
|
// SRT (SubRip) specification https://en.wikipedia.org/wiki/SubRip
|
||||||
|
|
||||||
namespace ConvertSRTto3DASS
|
namespace ConvertSRTto3DASS
|
||||||
{
|
{
|
||||||
class Converter
|
class Converter
|
||||||
{
|
{
|
||||||
|
private enum StereoMode
|
||||||
|
{
|
||||||
|
SBS,
|
||||||
|
OU
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Options
|
||||||
|
{
|
||||||
|
public string InputPath { get; set; }
|
||||||
|
public string OutputPath { get; set; }
|
||||||
|
public StereoMode Mode { get; set; } = StereoMode.SBS;
|
||||||
|
public int ResX { get; set; } = 384;
|
||||||
|
public int ResY { get; set; } = 288;
|
||||||
|
public int FontSize { get; set; } = 16;
|
||||||
|
}
|
||||||
|
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = ParseArguments(args);
|
||||||
|
|
||||||
var extracted = ExtractSubFromSRT(args[0]);
|
if (options == null)
|
||||||
var style = CreateStandardStyle();
|
return;
|
||||||
var header = CreateHeader(args[0]);
|
|
||||||
var events = ProcessSubs(extracted);
|
|
||||||
|
|
||||||
var finished = "[Script Info]\n" + header + "\n\n[V4+ Styles]\n" + style + "\n\n[Events]\n" + events;
|
if (!File.Exists(options.InputPath))
|
||||||
File.WriteAllText(Path.GetFileNameWithoutExtension(args[0]) + ".ass", finished, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
|
{
|
||||||
|
Console.Error.WriteLine("Input file not found: " + options.InputPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(options.OutputPath))
|
||||||
|
{
|
||||||
|
options.OutputPath = Path.Combine(
|
||||||
|
Path.GetDirectoryName(options.InputPath) ?? "",
|
||||||
|
Path.GetFileNameWithoutExtension(options.InputPath) + ".ass");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Input: " + options.InputPath);
|
||||||
|
Console.WriteLine("Output: " + options.OutputPath);
|
||||||
|
Console.WriteLine("Mode: " + options.Mode);
|
||||||
|
Console.WriteLine("Resolution:" + " " + options.ResX + "x" + options.ResY);
|
||||||
|
Console.WriteLine("Font size: " + options.FontSize);
|
||||||
|
|
||||||
|
var extracted = ExtractSubFromSRT(options.InputPath);
|
||||||
|
Console.WriteLine("Subtitle blocks parsed: " + extracted.Count);
|
||||||
|
|
||||||
|
var style = CreateStandardStyle(options.Mode, options.FontSize);
|
||||||
|
var header = CreateHeader(options.InputPath, options.ResY, options.ResX);
|
||||||
|
var eventsText = ProcessSubs(extracted, options.Mode);
|
||||||
|
|
||||||
|
var finished =
|
||||||
|
"[Script Info]\n" +
|
||||||
|
header +
|
||||||
|
"\n\n[V4+ Styles]\n" +
|
||||||
|
style +
|
||||||
|
"\n\n[Events]\n" +
|
||||||
|
eventsText;
|
||||||
|
|
||||||
|
File.WriteAllText(options.OutputPath, finished, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
|
||||||
|
|
||||||
|
Console.WriteLine("Conversion complete.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("ERROR: " + ex.Message);
|
||||||
|
Console.Error.WriteLine(ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Options ParseArguments(string[] args)
|
||||||
|
{
|
||||||
|
if (args == null || args.Length == 0)
|
||||||
|
{
|
||||||
|
PrintUsage();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new Options();
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
// First non-option argument is input file
|
||||||
|
if (args[0].StartsWith("--"))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("First argument must be the input .srt file.");
|
||||||
|
PrintUsage();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.InputPath = args[0];
|
||||||
|
i = 1;
|
||||||
|
|
||||||
|
while (i < args.Length)
|
||||||
|
{
|
||||||
|
string arg = args[i].Trim();
|
||||||
|
|
||||||
|
switch (arg.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "--mode":
|
||||||
|
EnsureValueExists(args, i, "--mode");
|
||||||
|
options.Mode = ParseStereoMode(args[i + 1]);
|
||||||
|
i += 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--resx":
|
||||||
|
EnsureValueExists(args, i, "--resx");
|
||||||
|
options.ResX = ParsePositiveInt(args[i + 1], "--resx");
|
||||||
|
i += 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--resy":
|
||||||
|
EnsureValueExists(args, i, "--resy");
|
||||||
|
options.ResY = ParsePositiveInt(args[i + 1], "--resy");
|
||||||
|
i += 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--fontsize":
|
||||||
|
EnsureValueExists(args, i, "--fontsize");
|
||||||
|
options.FontSize = ParsePositiveInt(args[i + 1], "--fontsize");
|
||||||
|
i += 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--output":
|
||||||
|
EnsureValueExists(args, i, "--output");
|
||||||
|
options.OutputPath = args[i + 1];
|
||||||
|
i += 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
case "/?":
|
||||||
|
PrintUsage();
|
||||||
|
return null;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentException("Unknown argument: " + arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureValueExists(string[] args, int index, string optionName)
|
||||||
|
{
|
||||||
|
if (index + 1 >= args.Length || args[index + 1].StartsWith("--"))
|
||||||
|
throw new ArgumentException("Missing value for " + optionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ParsePositiveInt(string value, string optionName)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(value, out int parsed) || parsed <= 0)
|
||||||
|
throw new ArgumentException("Invalid value for " + optionName + ": " + value);
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintUsage()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Usage:");
|
||||||
|
Console.WriteLine(" ConvertSRTto3DASS.exe <input.srt> [options]");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Options:");
|
||||||
|
Console.WriteLine(" --mode sbs|ou");
|
||||||
|
Console.WriteLine(" --resx <number>");
|
||||||
|
Console.WriteLine(" --resy <number>");
|
||||||
|
Console.WriteLine(" --fontsize <number>");
|
||||||
|
Console.WriteLine(" --output <path>");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Examples:");
|
||||||
|
Console.WriteLine(@" ConvertSRTto3DASS.exe ""movie.srt"" --mode sbs");
|
||||||
|
Console.WriteLine(@" ConvertSRTto3DASS.exe ""movie.srt"" --mode ou --resx 384 --resy 288");
|
||||||
|
Console.WriteLine(@" ConvertSRTto3DASS.exe ""movie.srt"" --mode sbs --fontsize 20");
|
||||||
|
Console.WriteLine(@" ConvertSRTto3DASS.exe ""movie.srt"" --mode ou --output ""movie_ou.ass""");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StereoMode ParseStereoMode(string mode)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(mode))
|
||||||
|
return StereoMode.SBS;
|
||||||
|
|
||||||
|
switch (mode.Trim().ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "sbs":
|
||||||
|
case "sidebyside":
|
||||||
|
case "side-by-side":
|
||||||
|
return StereoMode.SBS;
|
||||||
|
|
||||||
|
case "ou":
|
||||||
|
case "overunder":
|
||||||
|
case "over-under":
|
||||||
|
case "tab":
|
||||||
|
case "topandbottom":
|
||||||
|
case "top-and-bottom":
|
||||||
|
return StereoMode.OU;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Invalid 3D type '" + mode + "'. Valid values are: sbs, ou");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Is there a better way to do it?
|
|
||||||
private static Dictionary<Regex, string> regexReplacementDict =
|
private static Dictionary<Regex, string> regexReplacementDict =
|
||||||
new Dictionary<Regex, string> {
|
new Dictionary<Regex, string> {
|
||||||
{new Regex("<b>"), "{\\b1}"},
|
{ new Regex("<b>", RegexOptions.IgnoreCase), "{\\b1}" },
|
||||||
{new Regex("(</b>)"),"{\\b0}"},
|
{ new Regex("</b>", RegexOptions.IgnoreCase), "{\\b0}" },
|
||||||
{new Regex("(<i>)"),"{\\i1}"},
|
{ new Regex("<i>", RegexOptions.IgnoreCase), "{\\i1}" },
|
||||||
{new Regex("(</i>)"),"{\\i0}"},
|
{ new Regex("</i>", RegexOptions.IgnoreCase), "{\\i0}" },
|
||||||
{new Regex("(<u>)"),"{\\u1}" },
|
{ new Regex("<u>", RegexOptions.IgnoreCase), "{\\u1}" },
|
||||||
{new Regex("(</u>)"),"{\\u0}"},
|
{ new Regex("</u>", RegexOptions.IgnoreCase), "{\\u0}" },
|
||||||
{new Regex("(</font>)"),"{\\c&HFFFFFF&}"} //TODO custom colors. When you add custom colors, it needs to change this too
|
{ new Regex("</font>", RegexOptions.IgnoreCase), "{\\c&HFFFFFF&}" }
|
||||||
|
|
||||||
};
|
};
|
||||||
private static Regex color = new Regex("<font color=\"#.{6}\">");
|
|
||||||
|
|
||||||
//TODO Add positional data to the formatting.
|
private static Regex color = new Regex("<font color=\"#([0-9A-Fa-f]{6})\">", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
private static string ChangeFormatting(string line)
|
private static string ChangeFormatting(string line)
|
||||||
{
|
{
|
||||||
foreach (var tuple in regexReplacementDict)
|
foreach (var tuple in regexReplacementDict)
|
||||||
@@ -50,68 +236,69 @@ namespace ConvertSRTto3DASS
|
|||||||
while (color.IsMatch(line))
|
while (color.IsMatch(line))
|
||||||
{
|
{
|
||||||
var match = color.Match(line);
|
var match = color.Match(line);
|
||||||
string str_match = match.Value;
|
string rgb = match.Groups[1].Value; // RRGGBB
|
||||||
var color_str = str_match.Substring(14, 6); //RGB value in HEX
|
|
||||||
|
|
||||||
//ASS uses RGB value, but in reverse order so got to reverse it
|
string rr = rgb.Substring(0, 2);
|
||||||
var char_array = color_str.ToCharArray();
|
string gg = rgb.Substring(2, 2);
|
||||||
Array.Reverse(char_array);
|
string bb = rgb.Substring(4, 2);
|
||||||
color_str = new string(char_array);
|
|
||||||
|
|
||||||
line = color.Replace(line, "{\\c&" + color_str + "&}");
|
|
||||||
|
|
||||||
|
// ASS expects BBGGRR
|
||||||
|
string assColor = bb + gg + rr;
|
||||||
|
|
||||||
|
line = color.Replace(line, "{\\c&H" + assColor + "&}", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return removeFormatting(line);
|
return RemoveFormatting(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Regex reg = new Regex("<.+?>|(\\r)", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
//TODO: remove this and use the above method instead where it actually uses the formatting of the file
|
private static string RemoveFormatting(string line)
|
||||||
private static Regex reg = new Regex("<.+?>|(\\r)"); //Used to remove html tags used and line breaks
|
|
||||||
private static string removeFormatting(string line)
|
|
||||||
{
|
{
|
||||||
var replacement = reg.Replace(line, "");
|
var replacement = reg.Replace(line, "");
|
||||||
return replacement;
|
return replacement;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ProcessSubs(List<Tuple<string, string, string, string>> srt, StereoMode mode)
|
||||||
private static string ProcessSubs(List<Tuple<string, string, string, string>> srt)
|
|
||||||
{
|
{
|
||||||
string endResult = "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n";
|
string endResult = "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n";
|
||||||
|
|
||||||
|
string style1 = mode == StereoMode.SBS ? "Right" : "Top";
|
||||||
|
string style2 = mode == StereoMode.SBS ? "Left" : "Bottom";
|
||||||
|
|
||||||
foreach (var events in srt)
|
foreach (var events in srt)
|
||||||
{
|
{
|
||||||
var start = ConvertTimeStamp(events.Item1);
|
var start = ConvertTimeStamp(events.Item1);
|
||||||
var end = ConvertTimeStamp(events.Item2);
|
var end = ConvertTimeStamp(events.Item2);
|
||||||
|
|
||||||
//First layer, for the right eye
|
var line = "Dialogue: 0," + start + "," + end + "," + style1 + ",,0,0,0,," + events.Item3 + "\n";
|
||||||
var line = "Dialogue: " + 0 + "," + start + "," + end + ",Right,,0,0,0,," + events.Item3 + "\n";
|
line += "Dialogue: 1," + start + "," + end + "," + style2 + ",,0,0,0,," + events.Item3;
|
||||||
//Second layer, for the left eye
|
|
||||||
line += "Dialogue: " + 1 + "," + start + "," + end + ",Left,,0,0,0,," + events.Item3;
|
|
||||||
|
|
||||||
endResult += line + "\n";
|
endResult += line + "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return endResult;
|
return endResult;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static string ConvertTimeStamp(string timeStamp)
|
private static string ConvertTimeStamp(string timeStamp)
|
||||||
{
|
{
|
||||||
//Change double digit hour (for .srt) to single digit hour marker (for .ass) <- very crude
|
|
||||||
var tmp = timeStamp.Substring(1);
|
var tmp = timeStamp.Substring(1);
|
||||||
|
tmp = tmp.Substring(0, tmp.Length - 1);
|
||||||
//Change thousands of a second hunderdth second
|
|
||||||
tmp = tmp.Substring(0, tmp.Length - 1);//TODO: Do actual rounding
|
|
||||||
return tmp.Replace(",", ".");
|
return tmp.Replace(",", ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Make this human readable
|
private static string CreateStandardStyle(StereoMode stereoMode, int fontSize)
|
||||||
//TODO: Add adjustable parameters
|
|
||||||
private static string CreateStandardStyle()
|
|
||||||
{
|
{
|
||||||
string style = "Format: " +
|
if (stereoMode == StereoMode.SBS)
|
||||||
|
return CreateSbsStyle(fontSize);
|
||||||
|
|
||||||
|
return CreateOuStyle(fontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateSbsStyle(int fontSize)
|
||||||
|
{
|
||||||
|
string style =
|
||||||
|
"Format: " +
|
||||||
"Name, " +
|
"Name, " +
|
||||||
"Fontname, " +
|
"Fontname, " +
|
||||||
"Fontsize, " +
|
"Fontsize, " +
|
||||||
@@ -139,7 +326,7 @@ namespace ConvertSRTto3DASS
|
|||||||
"Style: " +
|
"Style: " +
|
||||||
"Right," +
|
"Right," +
|
||||||
"Arial," +
|
"Arial," +
|
||||||
"16," +
|
fontSize + "," +
|
||||||
"&Hffffff," +
|
"&Hffffff," +
|
||||||
"&Hffffff," +
|
"&Hffffff," +
|
||||||
"&H0," +
|
"&H0," +
|
||||||
@@ -161,11 +348,10 @@ namespace ConvertSRTto3DASS
|
|||||||
"10," +
|
"10," +
|
||||||
"0\n" +
|
"0\n" +
|
||||||
|
|
||||||
|
|
||||||
"Style: " +
|
"Style: " +
|
||||||
"Left," +
|
"Left," +
|
||||||
"Arial," +
|
"Arial," +
|
||||||
"16," +
|
fontSize + "," +
|
||||||
"&Hffffff," +
|
"&Hffffff," +
|
||||||
"&Hffffff," +
|
"&Hffffff," +
|
||||||
"&H0," +
|
"&H0," +
|
||||||
@@ -183,34 +369,115 @@ namespace ConvertSRTto3DASS
|
|||||||
"0," +
|
"0," +
|
||||||
"2," +
|
"2," +
|
||||||
"0," +
|
"0," +
|
||||||
"192 ," +
|
"192," +
|
||||||
"10," +
|
"10," +
|
||||||
"0";
|
"0";
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateOuStyle(int fontSize)
|
||||||
|
{
|
||||||
|
string style =
|
||||||
|
"Format: " +
|
||||||
|
"Name, " +
|
||||||
|
"Fontname, " +
|
||||||
|
"Fontsize, " +
|
||||||
|
"PrimaryColour, " +
|
||||||
|
"SecondaryColour, " +
|
||||||
|
"OutlineColour, " +
|
||||||
|
"BackColour, " +
|
||||||
|
"Bold, " +
|
||||||
|
"Italic, " +
|
||||||
|
"Underline, " +
|
||||||
|
"StrikeOut, " +
|
||||||
|
"ScaleX, " +
|
||||||
|
"ScaleY, " +
|
||||||
|
"Spacing, " +
|
||||||
|
"Angle, " +
|
||||||
|
"BorderStyle, " +
|
||||||
|
"Outline, " +
|
||||||
|
"Shadow, " +
|
||||||
|
"Alignment, " +
|
||||||
|
"MarginL, " +
|
||||||
|
"MarginR, " +
|
||||||
|
"MarginV, " +
|
||||||
|
"Encoding\n" +
|
||||||
|
|
||||||
|
"Style: " +
|
||||||
|
"Top," +
|
||||||
|
"Arial," +
|
||||||
|
fontSize + "," +
|
||||||
|
"&Hffffff," +
|
||||||
|
"&Hffffff," +
|
||||||
|
"&H0," +
|
||||||
|
"&H0," +
|
||||||
|
"0," +
|
||||||
|
"0," +
|
||||||
|
"0," +
|
||||||
|
"0," +
|
||||||
|
"100," +
|
||||||
|
"100," +
|
||||||
|
"0," +
|
||||||
|
"0," +
|
||||||
|
"1," +
|
||||||
|
"1," +
|
||||||
|
"0," +
|
||||||
|
"2," +
|
||||||
|
"0," +
|
||||||
|
"0," +
|
||||||
|
"154," +
|
||||||
|
"0\n" +
|
||||||
|
|
||||||
|
"Style: " +
|
||||||
|
"Bottom," +
|
||||||
|
"Arial," +
|
||||||
|
fontSize + "," +
|
||||||
|
"&Hffffff," +
|
||||||
|
"&Hffffff," +
|
||||||
|
"&H0," +
|
||||||
|
"&H0," +
|
||||||
|
"0," +
|
||||||
|
"0," +
|
||||||
|
"0," +
|
||||||
|
"0," +
|
||||||
|
"100," +
|
||||||
|
"100," +
|
||||||
|
"0," +
|
||||||
|
"0," +
|
||||||
|
"1," +
|
||||||
|
"1," +
|
||||||
|
"0," +
|
||||||
|
"2," +
|
||||||
|
"0," +
|
||||||
|
"0," +
|
||||||
|
"10," +
|
||||||
|
"0";
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: create a system where you can actually give paramaters to it
|
|
||||||
private static string CreateHeader(string file, int resY = 288, int resX = 384)
|
private static string CreateHeader(string file, int resY = 288, int resX = 384)
|
||||||
{
|
{
|
||||||
var name = Path.GetFileNameWithoutExtension(file);
|
var name = Path.GetFileNameWithoutExtension(file);
|
||||||
string scriptinfo = "; Generated by ConvertSRTto3D\n" +
|
string scriptinfo =
|
||||||
|
"; Generated by ConvertSRTto3D\n" +
|
||||||
"Title: " + name + "\n" +
|
"Title: " + name + "\n" +
|
||||||
"ScriptType: v4.00+\n" +
|
"ScriptType: v4.00+\n" +
|
||||||
"Collisions: Normal\n" +
|
"Collisions: Normal\n" +
|
||||||
"PlayResX: " + resX + "\n" +
|
"PlayResX: " + resX + "\n" +
|
||||||
"PlayResY: " + resY + "\n" +
|
"PlayResY: " + resY + "\n" +
|
||||||
"ScaledBorderAndShadow: yes";
|
"ScaledBorderAndShadow: yes";
|
||||||
|
|
||||||
return scriptinfo;
|
return scriptinfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ReadTextSmart(string path)
|
private static string ReadTextSmart(string path)
|
||||||
{
|
{
|
||||||
// Try reading as UTF-8 with BOM detection
|
|
||||||
using (var reader = new StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true))
|
using (var reader = new StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true))
|
||||||
{
|
{
|
||||||
string text = reader.ReadToEnd();
|
string text = reader.ReadToEnd();
|
||||||
|
|
||||||
// If UTF-8 decoding produced replacement character <?>, fallback to CP1252
|
|
||||||
if (text.Contains('\uFFFD'))
|
if (text.Contains('\uFFFD'))
|
||||||
text = File.ReadAllText(path, Encoding.GetEncoding(1252));
|
text = File.ReadAllText(path, Encoding.GetEncoding(1252));
|
||||||
|
|
||||||
@@ -218,73 +485,65 @@ namespace ConvertSRTto3DASS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//start, end, text, format <- tuple format
|
|
||||||
//Note - I want to change the tuple as format bit is useless as the data is carried in the text field for both .srt and .ass
|
|
||||||
//but I could reprepose it for any positional data
|
|
||||||
private static List<Tuple<string, string, string, string>> ExtractSubFromSRT(string file)
|
private static List<Tuple<string, string, string, string>> ExtractSubFromSRT(string file)
|
||||||
{
|
{
|
||||||
|
var results = new List<Tuple<string, string, string, string>>();
|
||||||
var converted = new List<Tuple<string, string, string, string>>();
|
|
||||||
|
|
||||||
var timestamp_start = "";
|
|
||||||
var timestamp_end = "";
|
|
||||||
var subtitles = "";
|
|
||||||
string srt = ReadTextSmart(file);
|
string srt = ReadTextSmart(file);
|
||||||
|
|
||||||
|
srt = srt.Replace("\r\n", "\n").Replace("\r", "\n");
|
||||||
|
|
||||||
int dialogNumber = 1; // Which dialog we are on
|
var blocks = Regex.Split(srt.Trim(), @"\n\s*\n");
|
||||||
int dialogLine = 0; // Which line of the dialog block we are on
|
|
||||||
int linecounter = 0; //Where we are in the .srt, meant for debugging
|
|
||||||
|
|
||||||
foreach (string line in srt.Split('\n'))
|
int expectedDialogNumber = 1;
|
||||||
|
|
||||||
|
foreach (var block in blocks)
|
||||||
{
|
{
|
||||||
linecounter++;
|
var lines = block
|
||||||
//Empty line assumes that the next line with event number thus previous dialog is finished and can be saved
|
.Split(new[] { '\n' }, StringSplitOptions.None)
|
||||||
if (line == "" | line == "\r")
|
.Select(l => l.Trim())
|
||||||
|
.Where(l => l.Length > 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (lines.Count < 3)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!int.TryParse(lines[0], out int dialogNumber))
|
||||||
{
|
{
|
||||||
dialogLine = 0;
|
Console.Error.WriteLine($"Skipping malformed block: expected subtitle number, got [{lines[0]}]");
|
||||||
converted.Add(new Tuple<string, string, string, string>(timestamp_start, timestamp_end, ChangeFormatting(subtitles), ""));
|
|
||||||
subtitles = "";
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Handle the first line
|
if (dialogNumber != expectedDialogNumber)
|
||||||
if (dialogLine == 0)
|
|
||||||
{
|
{
|
||||||
//Check that the line is an actual number
|
Console.Error.WriteLine(
|
||||||
if (int.TryParse(line, out int k) && k == dialogNumber)
|
$"Warning: expected subtitle number {expectedDialogNumber}, but found {dialogNumber}.");
|
||||||
|
expectedDialogNumber = dialogNumber;
|
||||||
|
}
|
||||||
|
expectedDialogNumber++;
|
||||||
|
|
||||||
|
var timeMatch = Regex.Match(
|
||||||
|
lines[1],
|
||||||
|
@"^(?<start>\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(?<end>\d{2}:\d{2}:\d{2},\d{3})$");
|
||||||
|
|
||||||
|
if (!timeMatch.Success)
|
||||||
{
|
{
|
||||||
dialogNumber++;
|
Console.Error.WriteLine($"Skipping malformed timestamp line: [{lines[1]}]");
|
||||||
dialogLine++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine("Something went wrong");
|
string timestampStart = timeMatch.Groups["start"].Value;
|
||||||
Console.Error.WriteLine("Something wrong on line:" + linecounter);
|
string timestampEnd = timeMatch.Groups["end"].Value;
|
||||||
System.Environment.Exit(-1);
|
|
||||||
|
string subtitleText = string.Join("\\N", lines.Skip(2));
|
||||||
|
|
||||||
|
results.Add(new Tuple<string, string, string, string>(
|
||||||
|
timestampStart,
|
||||||
|
timestampEnd,
|
||||||
|
ChangeFormatting(subtitleText),
|
||||||
|
""));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Timestamp extraction
|
return results;
|
||||||
if (dialogLine == 1)
|
|
||||||
{
|
|
||||||
timestamp_start = line.Substring(0, 12);
|
|
||||||
timestamp_end = line.Substring(17, 12);
|
|
||||||
dialogLine++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (subtitles == "")
|
|
||||||
{
|
|
||||||
subtitles = line;
|
|
||||||
subtitles = subtitles.Replace("\\.r", "");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
subtitles = subtitles + "\\N" + line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return converted;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user