mirror of
https://github.com/imrayya/SRTto3Dsubtitles.git
synced 2026-09-21 10:17:27 +02:00
Compare commits
12 Commits
v0.11-alpha
..
v0.14
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b7b8df874 | |||
| 40ce11d068 | |||
| 743900f2f7 | |||
| fb75071c44 | |||
| 580c0a534d | |||
| ea0574ffb2 | |||
| 5e4e907a2c | |||
| 470e23291f | |||
| 327bc071aa | |||
| 9d51c5f554 | |||
| b3f9bfb074 | |||
| 28f9e4f8f9 |
@@ -0,0 +1,153 @@
|
||||
using System.Text;
|
||||
|
||||
namespace ConvertSRTto3DASS;
|
||||
|
||||
internal static class Cli
|
||||
{
|
||||
public static bool Run(string[] args)
|
||||
{
|
||||
if (args.Length == 0 || args[0] is "--help" or "-h" or "/?")
|
||||
{
|
||||
PrintUsage();
|
||||
return false;
|
||||
}
|
||||
|
||||
var inputPath = args[0];
|
||||
if (!File.Exists(inputPath))
|
||||
{
|
||||
Console.Error.WriteLine($"Input file not found: {inputPath}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse options
|
||||
var options = new ConversionOptions
|
||||
{
|
||||
InputPath = inputPath,
|
||||
Mode = "sbs",
|
||||
ResX = 1280,
|
||||
ResY = 720,
|
||||
BaseResX = 1280,
|
||||
BaseResY = 720,
|
||||
FontSize = 16,
|
||||
OffsetX = 4,
|
||||
BottomOffset = 18,
|
||||
SbsSideMargin = 640,
|
||||
OuTopMargin = 385,
|
||||
VerticalMargin = 25,
|
||||
};
|
||||
|
||||
int i = 1;
|
||||
while (i < args.Length)
|
||||
{
|
||||
var arg = args[i].Trim().ToLowerInvariant();
|
||||
switch (arg)
|
||||
{
|
||||
case "--mode":
|
||||
options.Mode = GetNextArg(i++);
|
||||
break;
|
||||
case "--resx":
|
||||
options.ResX = ParseInt(i++);
|
||||
break;
|
||||
case "--resy":
|
||||
options.ResY = ParseInt(i++);
|
||||
break;
|
||||
case "--baseresx":
|
||||
options.BaseResX = ParseInt(i++);
|
||||
break;
|
||||
case "--baseresy":
|
||||
options.BaseResY = ParseInt(i++);
|
||||
break;
|
||||
case "--fontsize":
|
||||
options.FontSize = ParseInt(i++);
|
||||
break;
|
||||
case "--offsetx":
|
||||
options.OffsetX = ParseInt(i++);
|
||||
break;
|
||||
case "--bottomoffset":
|
||||
options.BottomOffset = ParseInt(i++);
|
||||
break;
|
||||
case "--sbssidemargin":
|
||||
options.SbsSideMargin = ParseInt(i++);
|
||||
break;
|
||||
case "--outopmargin":
|
||||
options.OuTopMargin = ParseInt(i++);
|
||||
break;
|
||||
case "--verticalmargin":
|
||||
options.VerticalMargin = ParseInt(i++);
|
||||
break;
|
||||
case "--output":
|
||||
options.OutputPath = GetNextArg(i++);
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown argument: {args[i]}");
|
||||
return true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// Default output path
|
||||
if (string.IsNullOrEmpty(options.OutputPath))
|
||||
{
|
||||
options.OutputPath = Path.Combine(
|
||||
Path.GetDirectoryName(options.InputPath) ?? ".",
|
||||
Path.GetFileNameWithoutExtension(options.InputPath) + ".ass");
|
||||
}
|
||||
|
||||
// Run conversion
|
||||
try
|
||||
{
|
||||
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($"Base resolution: {options.BaseResX}x{options.BaseResY}");
|
||||
Console.WriteLine($"Font size: {options.FontSize}");
|
||||
|
||||
var result = SrtConverter.Convert(options);
|
||||
Console.WriteLine($"Parsed: {result.SubtitleCount} subtitle blocks");
|
||||
Console.WriteLine("Conversion complete.");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"ERROR: {ex.Message}");
|
||||
Console.Error.WriteLine(ex.ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
string GetNextArg(int index) => index + 1 < args.Length ? args[index + 1] : throw new ArgumentException($"Missing value for {args[index]}");
|
||||
int ParseInt(int index)
|
||||
{
|
||||
var val = index + 1 < args.Length ? args[index + 1] : throw new ArgumentException($"Missing value for {args[index]}");
|
||||
return int.TryParse(val, out var n) ? n : throw new ArgumentException($"Invalid value for {args[index]}: {val}");
|
||||
}
|
||||
}
|
||||
|
||||
static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine("Usage:");
|
||||
Console.WriteLine(" ConvertSRTto3DASS.exe <input.srt> [options]");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Options:");
|
||||
Console.WriteLine(" --mode sbs|ou|rg");
|
||||
Console.WriteLine(" --resx <number>");
|
||||
Console.WriteLine(" --resy <number>");
|
||||
Console.WriteLine(" --baseresx <number> (scaling reference width, default 1280)");
|
||||
Console.WriteLine(" --baseresy <number> (scaling reference height, default 720)");
|
||||
Console.WriteLine(" --fontsize <number>");
|
||||
Console.WriteLine(" --offsetx <number> (RG eye separation)");
|
||||
Console.WriteLine(" --bottomoffset <number> (RG bottom offset)");
|
||||
Console.WriteLine(" --sbssidemargin <number> (SBS side margin)");
|
||||
Console.WriteLine(" --outopmargin <number> (OU top subtitle margin)");
|
||||
Console.WriteLine(" --verticalmargin <number> (general vertical/bottom margin)");
|
||||
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 1280 --resy 720");
|
||||
Console.WriteLine(@" ConvertSRTto3DASS.exe ""movie.srt"" --mode rg --offsetx 6 --bottomoffset 24");
|
||||
Console.WriteLine(@" ConvertSRTto3DASS.exe ""movie.srt"" --resx 1920 --resy 1080 --baseresx 1280 --baseresy 720");
|
||||
Console.WriteLine(@" ConvertSRTto3DASS.exe ""movie.srt"" --sbssidemargin 220 --verticalmargin 16");
|
||||
Console.WriteLine(@" ConvertSRTto3DASS.exe ""movie.srt"" --outopmargin 180 --fontsize 20 --output ""movie_custom.ass""");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Company>imrayya</Company>
|
||||
<Copyright>Copyright © imrayya 2021-2026</Copyright>
|
||||
<AssemblyName>ConvertSRTto3DASS</AssemblyName>
|
||||
<RootNamespace>ConvertSRTto3DASS</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<Optimize>true</Optimize>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<StripSymbols>true</StripSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ConvertSRTto3DASS.Core\ConvertSRTto3DASS.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ConvertSRTto3DASS;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var hadError = Cli.Run(args);
|
||||
if (hadError)
|
||||
Console.ReadLine();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace ConvertSRTto3DASS;
|
||||
|
||||
public class ConversionOptions
|
||||
{
|
||||
public string InputPath { get; set; } = "";
|
||||
public string OutputPath { get; set; } = "";
|
||||
|
||||
public string Mode { get; set; } = "sbs"; // sbs, ou, rg
|
||||
|
||||
public int ResX { get; set; } = 1280;
|
||||
public int ResY { get; set; } = 720;
|
||||
public int BaseResX { get; set; } = 1280;
|
||||
public int BaseResY { get; set; } = 720;
|
||||
|
||||
public int FontSize { get; set; } = 16;
|
||||
public int OffsetX { get; set; } = 4;
|
||||
public int BottomOffset { get; set; } = 18;
|
||||
public int SbsSideMargin { get; set; } = 640;
|
||||
public int OuTopMargin { get; set; } = 385;
|
||||
public int VerticalMargin { get; set; } = 25;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Company>imrayya</Company>
|
||||
<Copyright>Copyright © imrayya 2021-2026</Copyright>
|
||||
<RootNamespace>ConvertSRTto3DASS</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,276 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ConvertSRTto3DASS;
|
||||
|
||||
/// <summary>
|
||||
/// Core SRT-to-ASS conversion logic for 3D subtitle formats.
|
||||
/// </summary>
|
||||
public static class SrtConverter
|
||||
{
|
||||
public class ConversionResult
|
||||
{
|
||||
public string OutputText { get; set; } = "";
|
||||
public int SubtitleCount { get; set; }
|
||||
}
|
||||
|
||||
public static ConversionResult Convert(ConversionOptions options)
|
||||
{
|
||||
// Register legacy codepage support (needed for CP1252, etc.)
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
var srtText = ReadSrt(options.InputPath);
|
||||
var subtitles = ParseSrt(srtText);
|
||||
var header = CreateHeader(options);
|
||||
var styles = CreateStyles(options);
|
||||
var events = GenerateEvents(subtitles, options);
|
||||
|
||||
var output =
|
||||
"[Script Info]\n" +
|
||||
header +
|
||||
"\n\n[V4+ Styles]\n" +
|
||||
styles +
|
||||
"\n\n[Events]\n" +
|
||||
events;
|
||||
|
||||
File.WriteAllText(options.OutputPath, output, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
|
||||
|
||||
return new ConversionResult
|
||||
{
|
||||
OutputText = output,
|
||||
SubtitleCount = subtitles.Count
|
||||
};
|
||||
}
|
||||
|
||||
#region SRT Parsing
|
||||
|
||||
private static string ReadSrt(string path)
|
||||
{
|
||||
using var reader = new StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||
var text = reader.ReadToEnd();
|
||||
|
||||
// Fallback to Windows-1252 if we see replacement characters
|
||||
if (text.Contains('\uFFFD'))
|
||||
text = File.ReadAllText(path, Encoding.GetEncoding(1252));
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private static List<SrtSubtitle> ParseSrt(string srtText)
|
||||
{
|
||||
var results = new List<SrtSubtitle>();
|
||||
srtText = srtText.Replace("\r\n", "\n").Replace("\r", "\n").Trim();
|
||||
|
||||
var blocks = Regex.Split(srtText, @"\n\s*\n");
|
||||
int expectedNumber = 1;
|
||||
|
||||
foreach (var block in blocks)
|
||||
{
|
||||
var lines = block.Split('\n', StringSplitOptions.TrimEntries)
|
||||
.Where(l => l.Length > 0)
|
||||
.ToList();
|
||||
|
||||
if (lines.Count < 3) continue;
|
||||
|
||||
if (!int.TryParse(lines[0], out var number)) continue;
|
||||
|
||||
if (number != expectedNumber)
|
||||
{
|
||||
Console.Error.WriteLine($"Warning: expected subtitle number {expectedNumber}, but found {number}.");
|
||||
expectedNumber = number;
|
||||
}
|
||||
expectedNumber++;
|
||||
|
||||
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)
|
||||
{
|
||||
Console.Error.WriteLine($"Skipping malformed timestamp: [{lines[1]}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = string.Join("\\N", lines.Skip(2));
|
||||
text = NormalizeFormatting(text);
|
||||
|
||||
results.Add(new SrtSubtitle
|
||||
{
|
||||
Start = timeMatch.Groups["start"].Value,
|
||||
End = timeMatch.Groups["end"].Value,
|
||||
Text = text
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Formatting
|
||||
|
||||
private static readonly Dictionary<Regex, string> _tagReplacements = new()
|
||||
{
|
||||
{ new Regex("<b>", RegexOptions.IgnoreCase), "{\\b1}" },
|
||||
{ new Regex("</b>", RegexOptions.IgnoreCase), "{\\b0}" },
|
||||
{ new Regex("<i>", RegexOptions.IgnoreCase), "{\\i1}" },
|
||||
{ new Regex("</i>", RegexOptions.IgnoreCase), "{\\i0}" },
|
||||
{ new Regex("<u>", RegexOptions.IgnoreCase), "{\\u1}" },
|
||||
{ new Regex("</u>", RegexOptions.IgnoreCase), "{\\u0}" },
|
||||
{ new Regex("</font>", RegexOptions.IgnoreCase), "{\\c&HFFFFFF&}" }
|
||||
};
|
||||
|
||||
private static readonly Regex _colorRegex =
|
||||
new Regex("<font color=\"#([0-9A-Fa-f]{6})\">", RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly Regex _markupRegex =
|
||||
new Regex("<.+?>|(\\r)", RegexOptions.IgnoreCase);
|
||||
|
||||
private static string NormalizeFormatting(string text)
|
||||
{
|
||||
foreach (var (pattern, replacement) in _tagReplacements)
|
||||
text = pattern.Replace(text, replacement);
|
||||
|
||||
// Convert HTML color to ASS BBGGRR format
|
||||
while (_colorRegex.IsMatch(text))
|
||||
{
|
||||
var match = _colorRegex.Match(text);
|
||||
var rgb = match.Groups[1].Value;
|
||||
var assColor = rgb.Substring(4, 2) + rgb.Substring(2, 2) + rgb.Substring(0, 2);
|
||||
text = _colorRegex.Replace(text, "{\\c&H" + assColor + "&}", 1);
|
||||
}
|
||||
|
||||
return _markupRegex.Replace(text, "");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Time & Scaling
|
||||
|
||||
private static string ConvertTimestamp(string timestamp)
|
||||
{
|
||||
var tmp = timestamp.Substring(1);
|
||||
tmp = tmp.Substring(0, tmp.Length - 1);
|
||||
return tmp.Replace(",", ".");
|
||||
}
|
||||
|
||||
private static int ScaleX(int value, ConversionOptions o) =>
|
||||
(int)Math.Round(value * (o.ResX / (double)o.BaseResX));
|
||||
|
||||
private static int ScaleY(int value, ConversionOptions o) =>
|
||||
(int)Math.Round(value * (o.ResY / (double)o.BaseResY));
|
||||
|
||||
private static int ScaleFont(int fontSize, ConversionOptions o) =>
|
||||
Math.Max(1, ScaleY(fontSize, o));
|
||||
|
||||
#endregion
|
||||
|
||||
#region ASS Output Generation
|
||||
|
||||
private static string CreateHeader(ConversionOptions o) =>
|
||||
$"; Generated by SRTto3Dsubtitles\n" +
|
||||
$"Title: {Path.GetFileNameWithoutExtension(o.InputPath)}\n" +
|
||||
"ScriptType: v4.00+\n" +
|
||||
"Collisions: Normal\n" +
|
||||
$"PlayResX: {o.ResX}\n" +
|
||||
$"PlayResY: {o.ResY}\n" +
|
||||
"ScaledBorderAndShadow: yes";
|
||||
|
||||
private static string CreateStyles(ConversionOptions o)
|
||||
{
|
||||
var fontSize = ScaleFont(o.FontSize, o);
|
||||
|
||||
return o.Mode.ToLowerInvariant() switch
|
||||
{
|
||||
"ou" => CreateOuStyles(fontSize, o),
|
||||
"rg" => CreateRgStyles(fontSize, o),
|
||||
_ => CreateSbsStyles(fontSize, o)
|
||||
};
|
||||
}
|
||||
|
||||
private static string CreateSbsStyles(int fontSize, ConversionOptions o)
|
||||
{
|
||||
var sideMargin = ScaleX(o.SbsSideMargin, o);
|
||||
var verticalMargin = ScaleY(o.VerticalMargin, o);
|
||||
|
||||
return FormatStyle("Right", fontSize, sideMargin, 0, verticalMargin) + "\n" +
|
||||
FormatStyle("Left", fontSize, 0, sideMargin, verticalMargin);
|
||||
}
|
||||
|
||||
private static string CreateOuStyles(int fontSize, ConversionOptions o)
|
||||
{
|
||||
var topMargin = ScaleY(o.OuTopMargin, o);
|
||||
var bottomMargin = ScaleY(o.VerticalMargin, o);
|
||||
|
||||
return FormatStyle("Top", fontSize, 0, 0, topMargin) + "\n" +
|
||||
FormatStyle("Bottom", fontSize, 0, 0, bottomMargin);
|
||||
}
|
||||
|
||||
private static string CreateRgStyles(int fontSize, ConversionOptions o)
|
||||
{
|
||||
var verticalMargin = ScaleY(o.VerticalMargin, o);
|
||||
|
||||
// Red = &H0000FF, Green = &H00FF00 (ASS uses BBGGRR)
|
||||
return FormatStyle("RedEye", fontSize, 0, 0, verticalMargin, primary: "&H0000FF") + "\n" +
|
||||
FormatStyle("GreenEye", fontSize, 0, 0, verticalMargin, primary: "&H00FF00");
|
||||
}
|
||||
|
||||
private static string FormatStyle(string name, int fontSize, int marginL, int marginR, int marginV,
|
||||
string primary = "&HFFFFFF")
|
||||
{
|
||||
return $"Style: {name},Arial,{fontSize},{primary},{primary},&H000000,&H000000," +
|
||||
$"0,0,0,0,100,100,0,0,1,1,0,2," +
|
||||
$"{marginL},{marginR},{marginV},0";
|
||||
}
|
||||
|
||||
private static string GenerateEvents(List<SrtSubtitle> subs, ConversionOptions o)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
|
||||
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
var start = ConvertTimestamp(sub.Start);
|
||||
var end = ConvertTimestamp(sub.End);
|
||||
var text = sub.Text;
|
||||
|
||||
switch (o.Mode.ToLowerInvariant())
|
||||
{
|
||||
case "sbs":
|
||||
sb.AppendLine($"Dialogue: 0,{start},{end},Right,,0,0,0,,{text}");
|
||||
sb.AppendLine($"Dialogue: 1,{start},{end},Left,,0,0,0,,{text}");
|
||||
break;
|
||||
|
||||
case "ou":
|
||||
sb.AppendLine($"Dialogue: 0,{start},{end},Top,,0,0,0,,{text}");
|
||||
sb.AppendLine($"Dialogue: 1,{start},{end},Bottom,,0,0,0,,{text}");
|
||||
break;
|
||||
|
||||
case "rg":
|
||||
var centerX = o.ResX / 2;
|
||||
var scaledBottomOffset = ScaleY(o.BottomOffset, o);
|
||||
var y = o.ResY - scaledBottomOffset;
|
||||
var scaledOffsetX = ScaleX(o.OffsetX, o);
|
||||
var redX = centerX - scaledOffsetX;
|
||||
var greenX = centerX + scaledOffsetX;
|
||||
|
||||
sb.AppendLine($"Dialogue: 0,{start},{end},RedEye,,0,0,0,,{{\\an2\\pos({redX},{y})}}{text}");
|
||||
sb.AppendLine($"Dialogue: 1,{start},{end},GreenEye,,0,0,0,,{{\\an2\\pos({greenX},{y})}}{text}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data Classes
|
||||
|
||||
private class SrtSubtitle
|
||||
{
|
||||
public string Start { get; set; } = "";
|
||||
public string End { get; set; } = "";
|
||||
public string Text { get; set; } = "";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Company>imrayya</Company>
|
||||
<Copyright>Copyright © imrayya 2021-2026</Copyright>
|
||||
<AssemblyName>ConvertSRTto3DASS</AssemblyName>
|
||||
<RootNamespace>ConvertSRTto3DASS</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ConvertSRTto3DASS.Core\ConvertSRTto3DASS.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Form1.Designer.cs">
|
||||
<DependentUpon>Form1.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Form1.resx">
|
||||
<DependentUpon>Form1.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Generated
+199
@@ -0,0 +1,199 @@
|
||||
namespace ConvertSRTto3DASS;
|
||||
|
||||
partial class Form1
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
private Label label_Title;
|
||||
private GroupBox groupBox_File;
|
||||
private Label label_InputFile;
|
||||
private TextBox textBox_InputFile;
|
||||
private Button button_BrowseInput;
|
||||
private Label label_OutputFile;
|
||||
private TextBox textBox_OutputFile;
|
||||
private Button button_BrowseOutput;
|
||||
private GroupBox groupBox_Mode;
|
||||
private Label label_Mode;
|
||||
private ComboBox comboBox_Mode;
|
||||
private GroupBox groupBox_Resolution;
|
||||
private Label label_ResX;
|
||||
private NumericUpDown numericUpDown_ResX;
|
||||
private Label label_ResY;
|
||||
private NumericUpDown numericUpDown_ResY;
|
||||
private Label label_BaseResX;
|
||||
private NumericUpDown numericUpDown_BaseResX;
|
||||
private Label label_BaseResY;
|
||||
private NumericUpDown numericUpDown_BaseResY;
|
||||
private GroupBox groupBox_Settings;
|
||||
private Label label_FontSize;
|
||||
private NumericUpDown numericUpDown_FontSize;
|
||||
private Label label_OffsetX;
|
||||
private NumericUpDown numericUpDown_OffsetX;
|
||||
private Label label_BottomOffset;
|
||||
private NumericUpDown numericUpDown_BottomOffset;
|
||||
private Label label_SbsSideMargin;
|
||||
private NumericUpDown numericUpDown_SbsSideMargin;
|
||||
private Label label_OuTopMargin;
|
||||
private NumericUpDown numericUpDown_OuTopMargin;
|
||||
private Label label_VerticalMargin;
|
||||
private NumericUpDown numericUpDown_VerticalMargin;
|
||||
private Button button_Convert;
|
||||
private TextBox textBox_Status;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && components != null)
|
||||
components.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
var segoe = new Font("Segoe UI", 9f);
|
||||
|
||||
SuspendLayout();
|
||||
|
||||
// ---- Title ----
|
||||
label_Title = new Label
|
||||
{
|
||||
Text = "SRT to 3D Subtitles",
|
||||
Font = new Font("Segoe UI", 14f, FontStyle.Bold),
|
||||
ForeColor = Color.FromArgb(30, 30, 30),
|
||||
Location = new Point(20, 15),
|
||||
AutoSize = true
|
||||
};
|
||||
|
||||
// ---- File section ----
|
||||
groupBox_File = new GroupBox
|
||||
{
|
||||
Text = "Files",
|
||||
Font = segoe,
|
||||
Location = new Point(20, 60),
|
||||
Size = new Size(1040, 80)
|
||||
};
|
||||
label_InputFile = new Label { Text = "Input SRT:", Font = segoe, Location = new Point(15, 25), AutoSize = true };
|
||||
textBox_InputFile = new TextBox { Location = new Point(100, 22), Size = new Size(780, 23) };
|
||||
button_BrowseInput = new Button { Text = "Browse...", Location = new Point(890, 20), Size = new Size(90, 25), Font = segoe, FlatStyle = FlatStyle.Flat };
|
||||
button_BrowseInput.Click += button_BrowseInput_Click;
|
||||
label_OutputFile = new Label { Text = "Output ASS:", Font = segoe, Location = new Point(15, 55), AutoSize = true };
|
||||
textBox_OutputFile = new TextBox { Location = new Point(100, 52), Size = new Size(780, 23) };
|
||||
button_BrowseOutput = new Button { Text = "Browse...", Location = new Point(890, 50), Size = new Size(90, 25), Font = segoe, FlatStyle = FlatStyle.Flat };
|
||||
button_BrowseOutput.Click += button_BrowseOutput_Click;
|
||||
groupBox_File.Controls.AddRange([label_InputFile, textBox_InputFile, button_BrowseInput, label_OutputFile, textBox_OutputFile, button_BrowseOutput]);
|
||||
|
||||
// ---- Mode section ----
|
||||
groupBox_Mode = new GroupBox
|
||||
{
|
||||
Text = "3D Mode",
|
||||
Font = segoe,
|
||||
Location = new Point(20, 150),
|
||||
Size = new Size(320, 60)
|
||||
};
|
||||
label_Mode = new Label { Text = "Format:", Font = segoe, Location = new Point(15, 22), AutoSize = true };
|
||||
comboBox_Mode = new ComboBox
|
||||
{
|
||||
Location = new Point(85, 19),
|
||||
Size = new Size(210, 25),
|
||||
Font = segoe,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList
|
||||
};
|
||||
comboBox_Mode.Items.AddRange(["SBS (Side-by-Side)", "OU (Over-Under)", "RG (Red-Green)"]);
|
||||
groupBox_Mode.Controls.AddRange([label_Mode, comboBox_Mode]);
|
||||
|
||||
// ---- Resolution section ----
|
||||
groupBox_Resolution = new GroupBox
|
||||
{
|
||||
Text = "Resolution",
|
||||
Font = segoe,
|
||||
Location = new Point(355, 150),
|
||||
Size = new Size(320, 60)
|
||||
};
|
||||
label_ResX = new Label { Text = "Output:", Font = segoe, Location = new Point(15, 22), AutoSize = true };
|
||||
numericUpDown_ResX = new NumericUpDown { Location = new Point(65, 20), Size = new Size(80, 23), Minimum = 1, Maximum = 10000, Value = 1280 };
|
||||
label_ResY = new Label { Text = "x", Font = segoe, Location = new Point(150, 22), AutoSize = true };
|
||||
numericUpDown_ResY = new NumericUpDown { Location = new Point(165, 20), Size = new Size(80, 23), Minimum = 1, Maximum = 10000, Value = 720 };
|
||||
label_BaseResX = new Label { Text = "Base:", Font = segoe, Location = new Point(255, 22), AutoSize = true };
|
||||
numericUpDown_BaseResX = new NumericUpDown { Location = new Point(290, 20), Size = new Size(80, 23), Minimum = 1, Maximum = 10000, Value = 1280 };
|
||||
label_BaseResY = new Label { Text = "x", Font = segoe, Location = new Point(375, 22), AutoSize = true };
|
||||
numericUpDown_BaseResY = new NumericUpDown { Location = new Point(390, 20), Size = new Size(80, 23), Minimum = 1, Maximum = 10000, Value = 720 };
|
||||
groupBox_Resolution.Controls.AddRange([label_ResX, numericUpDown_ResX, label_ResY, numericUpDown_ResY, label_BaseResX, numericUpDown_BaseResX, label_BaseResY, numericUpDown_BaseResY]);
|
||||
|
||||
// ---- Settings section ----
|
||||
groupBox_Settings = new GroupBox
|
||||
{
|
||||
Text = "Settings",
|
||||
Font = segoe,
|
||||
Location = new Point(20, 220),
|
||||
Size = new Size(1040, 120)
|
||||
};
|
||||
|
||||
var col1 = 15;
|
||||
var col2 = 160;
|
||||
var col3 = 310;
|
||||
var col4 = 460;
|
||||
var col5 = 610;
|
||||
var col6 = 760;
|
||||
var row = 25;
|
||||
var rowStep = 30;
|
||||
|
||||
label_FontSize = new Label { Text = "Font Size:", Font = segoe, Location = new Point(col1, row), AutoSize = true };
|
||||
numericUpDown_FontSize = new NumericUpDown { Location = new Point(col2, row - 2), Size = new Size(80, 23), Minimum = 1, Maximum = 200, Value = 16 };
|
||||
label_OffsetX = new Label { Text = "Offset X:", Font = segoe, Location = new Point(col3, row), AutoSize = true };
|
||||
numericUpDown_OffsetX = new NumericUpDown { Location = new Point(col4, row - 2), Size = new Size(80, 23), Minimum = 0, Maximum = 1000, Value = 4 };
|
||||
label_BottomOffset = new Label { Text = "Bottom Offset:", Font = segoe, Location = new Point(col5, row), AutoSize = true };
|
||||
numericUpDown_BottomOffset = new NumericUpDown { Location = new Point(col6, row - 2), Size = new Size(80, 23), Minimum = 0, Maximum = 1000, Value = 18 };
|
||||
row += rowStep;
|
||||
|
||||
label_SbsSideMargin = new Label { Text = "SBS Margin:", Font = segoe, Location = new Point(col1, row), AutoSize = true };
|
||||
numericUpDown_SbsSideMargin = new NumericUpDown { Location = new Point(col2, row - 2), Size = new Size(80, 23), Minimum = 0, Maximum = 5000, Value = 640 };
|
||||
label_OuTopMargin = new Label { Text = "OU Margin:", Font = segoe, Location = new Point(col3, row), AutoSize = true };
|
||||
numericUpDown_OuTopMargin = new NumericUpDown { Location = new Point(col4, row - 2), Size = new Size(80, 23), Minimum = 0, Maximum = 5000, Value = 385 };
|
||||
label_VerticalMargin = new Label { Text = "V Margin:", Font = segoe, Location = new Point(col5, row), AutoSize = true };
|
||||
numericUpDown_VerticalMargin = new NumericUpDown { Location = new Point(col6, row - 2), Size = new Size(80, 23), Minimum = 0, Maximum = 5000, Value = 25 };
|
||||
|
||||
groupBox_Settings.Controls.AddRange([
|
||||
label_FontSize, numericUpDown_FontSize, label_OffsetX, numericUpDown_OffsetX, label_BottomOffset, numericUpDown_BottomOffset,
|
||||
label_SbsSideMargin, numericUpDown_SbsSideMargin, label_OuTopMargin, numericUpDown_OuTopMargin, label_VerticalMargin, numericUpDown_VerticalMargin
|
||||
]);
|
||||
|
||||
// ---- Convert button ----
|
||||
button_Convert = new Button
|
||||
{
|
||||
Text = "Convert",
|
||||
Font = new Font("Segoe UI", 10f, FontStyle.Bold),
|
||||
Location = new Point(20, 355),
|
||||
Size = new Size(140, 36),
|
||||
FlatStyle = FlatStyle.Flat,
|
||||
BackColor = Color.FromArgb(0, 120, 215),
|
||||
ForeColor = Color.White,
|
||||
FlatAppearance = { BorderSize = 0 }
|
||||
};
|
||||
|
||||
// ---- Status ----
|
||||
textBox_Status = new TextBox
|
||||
{
|
||||
Location = new Point(20, 405),
|
||||
Size = new Size(1040, 180),
|
||||
Multiline = true,
|
||||
ReadOnly = true,
|
||||
ScrollBars = ScrollBars.Vertical,
|
||||
Font = new Font("Consolas", 8.5f),
|
||||
BackColor = Color.FromArgb(245, 245, 245)
|
||||
};
|
||||
|
||||
// ---- Form ----
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(1080, 610);
|
||||
MinimumSize = new Size(1100, 650);
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
MaximizeBox = false;
|
||||
AllowDrop = true;
|
||||
Text = "SRT to 3D Subtitles";
|
||||
Font = segoe;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
Controls.AddRange([label_Title, groupBox_File, groupBox_Mode, groupBox_Resolution, groupBox_Settings, button_Convert, textBox_Status]);
|
||||
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using System.IO;
|
||||
|
||||
namespace ConvertSRTto3DASS;
|
||||
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
public Form1()
|
||||
{
|
||||
InitializeComponent();
|
||||
comboBox_Mode.SelectedIndex = 0;
|
||||
UpdateModeUI();
|
||||
|
||||
// Drag-and-drop
|
||||
DragEnter += (s, e) =>
|
||||
{
|
||||
if (e.Data!.GetDataPresent(DataFormats.FileDrop))
|
||||
e.Effect = DragDropEffects.Copy;
|
||||
};
|
||||
|
||||
DragDrop += (s, e) =>
|
||||
{
|
||||
var files = (string[])e.Data!.GetData(DataFormats.FileDrop)!;
|
||||
if (files.Length > 0 && files[0].EndsWith(".srt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
textBox_InputFile.Text = files[0];
|
||||
if (string.IsNullOrWhiteSpace(textBox_OutputFile.Text))
|
||||
textBox_OutputFile.Text = Path.ChangeExtension(files[0], ".ass");
|
||||
AppendStatus("Dropped: " + files[0]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void button_BrowseInput_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var ofd = new OpenFileDialog
|
||||
{
|
||||
Filter = "SRT files (*.srt)|*.srt|All files (*.*)|*.*",
|
||||
Title = "Select input SRT file"
|
||||
};
|
||||
|
||||
if (ofd.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
textBox_InputFile.Text = ofd.FileName;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(textBox_OutputFile.Text))
|
||||
textBox_OutputFile.Text = Path.ChangeExtension(ofd.FileName, ".ass");
|
||||
|
||||
AppendStatus("Selected input file: " + ofd.FileName);
|
||||
}
|
||||
}
|
||||
|
||||
private void button_BrowseOutput_Click(object sender, EventArgs e)
|
||||
{
|
||||
using var sfd = new SaveFileDialog
|
||||
{
|
||||
Filter = "ASS files (*.ass)|*.ass|All files (*.*)|*.*",
|
||||
Title = "Select output ASS file",
|
||||
DefaultExt = "ass"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(textBox_InputFile.Text))
|
||||
{
|
||||
sfd.FileName = Path.GetFileNameWithoutExtension(textBox_InputFile.Text) + ".ass";
|
||||
sfd.InitialDirectory = Path.GetDirectoryName(textBox_InputFile.Text);
|
||||
}
|
||||
|
||||
if (sfd.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
textBox_OutputFile.Text = sfd.FileName;
|
||||
AppendStatus("Selected output file: " + sfd.FileName);
|
||||
}
|
||||
}
|
||||
|
||||
private void comboBox_Mode_SelectedIndexChanged(object sender, EventArgs e) => UpdateModeUI();
|
||||
|
||||
private string GetSelectedModeValue()
|
||||
{
|
||||
var selected = comboBox_Mode.SelectedItem?.ToString() ?? "sbs";
|
||||
var spaceIndex = selected.IndexOf(' ');
|
||||
return spaceIndex > 0 ? selected[..spaceIndex].Trim().ToLowerInvariant() : selected.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private void button_Convert_Click(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
textBox_Status.Clear();
|
||||
|
||||
var inputPath = textBox_InputFile.Text.Trim();
|
||||
var outputPath = textBox_OutputFile.Text.Trim();
|
||||
var mode = GetSelectedModeValue();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputPath))
|
||||
{
|
||||
MessageBox.Show("Please select an input SRT file.", "Missing Input", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(inputPath))
|
||||
{
|
||||
MessageBox.Show("The selected input file does not exist.", "Invalid Input", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
outputPath = Path.ChangeExtension(inputPath, ".ass");
|
||||
textBox_OutputFile.Text = outputPath;
|
||||
}
|
||||
|
||||
var options = new ConversionOptions
|
||||
{
|
||||
InputPath = inputPath,
|
||||
OutputPath = outputPath,
|
||||
Mode = mode,
|
||||
ResX = (int)numericUpDown_ResX.Value,
|
||||
ResY = (int)numericUpDown_ResY.Value,
|
||||
BaseResX = (int)numericUpDown_BaseResX.Value,
|
||||
BaseResY = (int)numericUpDown_BaseResY.Value,
|
||||
FontSize = (int)numericUpDown_FontSize.Value,
|
||||
OffsetX = (int)numericUpDown_OffsetX.Value,
|
||||
BottomOffset = (int)numericUpDown_BottomOffset.Value,
|
||||
SbsSideMargin = (int)numericUpDown_SbsSideMargin.Value,
|
||||
OuTopMargin = (int)numericUpDown_OuTopMargin.Value,
|
||||
VerticalMargin = (int)numericUpDown_VerticalMargin.Value
|
||||
};
|
||||
|
||||
AppendStatus("Starting conversion...");
|
||||
AppendStatus($"Input: {options.InputPath}");
|
||||
AppendStatus($"Output: {options.OutputPath}");
|
||||
AppendStatus($"Mode: {options.Mode}");
|
||||
AppendStatus($"Resolution: {options.ResX}x{options.ResY}");
|
||||
AppendStatus($"Base resolution: {options.BaseResX}x{options.BaseResY}");
|
||||
AppendStatus($"Font size: {options.FontSize}");
|
||||
|
||||
var result = SrtConverter.Convert(options);
|
||||
|
||||
AppendStatus($"Parsed: {result.SubtitleCount} subtitle blocks");
|
||||
AppendStatus("Conversion complete.");
|
||||
MessageBox.Show("Conversion complete.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendStatus($"ERROR: {ex.Message}");
|
||||
MessageBox.Show(ex.Message, "Conversion Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateModeUI()
|
||||
{
|
||||
var mode = GetSelectedModeValue();
|
||||
|
||||
numericUpDown_OffsetX.Enabled = mode == "rg";
|
||||
label_OffsetX.Enabled = mode == "rg";
|
||||
numericUpDown_BottomOffset.Enabled = mode == "rg";
|
||||
label_BottomOffset.Enabled = mode == "rg";
|
||||
numericUpDown_SbsSideMargin.Enabled = mode == "sbs";
|
||||
label_SbsSideMargin.Enabled = mode == "sbs";
|
||||
numericUpDown_OuTopMargin.Enabled = mode == "ou";
|
||||
label_OuTopMargin.Enabled = mode == "ou";
|
||||
}
|
||||
|
||||
private void AppendStatus(string message)
|
||||
{
|
||||
if (textBox_Status.TextLength > 0)
|
||||
textBox_Status.AppendText(Environment.NewLine);
|
||||
|
||||
textBox_Status.AppendText(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ConvertSRTto3DASS;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
ApplicationConfiguration.Initialize();
|
||||
Application.Run(new Form1());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ConvertSRTto3DASS.Properties
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources
|
||||
{
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if ((resourceMan == null))
|
||||
{
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ConvertSRTto3DASSGUI.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture
|
||||
{
|
||||
get
|
||||
{
|
||||
return resourceCulture;
|
||||
}
|
||||
set
|
||||
{
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
+13
-9
@@ -8,17 +8,21 @@
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ConvertSRTto3DASS.Properties {
|
||||
|
||||
|
||||
namespace ConvertSRTto3DASS.Properties
|
||||
{
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
|
||||
{
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default {
|
||||
get {
|
||||
|
||||
public static Settings Default
|
||||
{
|
||||
get
|
||||
{
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
+1
@@ -3,4 +3,5 @@
|
||||
<Profiles>
|
||||
<Profile Name="(Default)" />
|
||||
</Profiles>
|
||||
<Settings />
|
||||
</SettingsFile>
|
||||
+20
-14
@@ -1,9 +1,13 @@
|
||||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30523.141
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConvertSRTto3DASS", "ConvertSRTto3DASS\ConvertSRTto3DASS.csproj", "{7D7925DE-ADBD-4A26-B12B-8F12B68D3BFE}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConvertSRTto3DASS.Core", "ConvertSRTto3DASS.Core\ConvertSRTto3DASS.Core.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConvertSRTto3DASS.Cli", "ConvertSRTto3DASS.Cli\ConvertSRTto3DASS.Cli.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConvertSRTto3DASS.Gui", "ConvertSRTto3DASS.Gui\ConvertSRTto3DASS.Gui.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -11,15 +15,17 @@ Global
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{7D7925DE-ADBD-4A26-B12B-8F12B68D3BFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7D7925DE-ADBD-4A26-B12B-8F12B68D3BFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7D7925DE-ADBD-4A26-B12B-8F12B68D3BFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7D7925DE-ADBD-4A26-B12B-8F12B68D3BFE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B5E10CFB-7432-4C4B-80FB-1B9349811932}
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net46</TargetFramework>
|
||||
<StartupObject>ConvertSRTto3DASS.Converter</StartupObject>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<RepositoryType>Git</RepositoryType>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<Optimize>true</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Settings.Designer.cs">
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,277 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
//SSA/ASS specification v4+ http://www.tcax.org/docs/ass-specs.htm
|
||||
//SRT (SubRip) specification https://en.wikipedia.org/wiki/SubRip
|
||||
|
||||
namespace ConvertSRTto3DASS
|
||||
{
|
||||
class Converter
|
||||
{
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
|
||||
var extracted = ExtractSubFromSRT(args[0]);
|
||||
var style = CreateStandardStyle();
|
||||
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;
|
||||
File.WriteAllText(Path.GetFileNameWithoutExtension(args[0]) + ".ass", finished);
|
||||
}
|
||||
|
||||
//Is there a better way to do it?
|
||||
private static Dictionary<Regex, string> regexReplacementDict =
|
||||
new Dictionary<Regex, string> {
|
||||
{new Regex("<b>"), "{\\b1}"},
|
||||
{new Regex("(</b>)"),"{\\b0}"},
|
||||
{new Regex("(<i>)"),"{\\i1}"},
|
||||
{new Regex("(</i>)"),"{\\i0}"},
|
||||
{new Regex("(<u>)"),"{\\u1}" },
|
||||
{new Regex("(</u>)"),"{\\u0}"},
|
||||
{new Regex("(</font>)"),"{\\c&HFFFFFF&}"} //TODO custom colors. When you add custom colors, it needs to change this too
|
||||
|
||||
};
|
||||
private static Regex color = new Regex("<font color=\"#.{6}\">");
|
||||
|
||||
//TODO Add positional data to the formatting.
|
||||
private static string ChangeFormatting(string line)
|
||||
{
|
||||
foreach (var tuple in regexReplacementDict)
|
||||
{
|
||||
line = tuple.Key.Replace(line, tuple.Value);
|
||||
}
|
||||
|
||||
while (color.IsMatch(line))
|
||||
{
|
||||
var match = color.Match(line);
|
||||
string str_match = match.Value;
|
||||
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
|
||||
var char_array = color_str.ToCharArray();
|
||||
Array.Reverse(char_array);
|
||||
color_str = new string(char_array);
|
||||
|
||||
line = color.Replace(line, "{\\c&" + color_str + "&}");
|
||||
|
||||
|
||||
}
|
||||
|
||||
return removeFormatting(line);
|
||||
}
|
||||
|
||||
|
||||
//TODO: remove this and use the above method instead where it actually uses the formatting of the file
|
||||
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, "");
|
||||
return replacement;
|
||||
|
||||
}
|
||||
|
||||
|
||||
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";
|
||||
foreach (var events in srt)
|
||||
{
|
||||
var start = ConvertTimeStamp(events.Item1);
|
||||
var end = ConvertTimeStamp(events.Item2);
|
||||
|
||||
//First layer, for the right eye
|
||||
var line = "Dialogue: " + 0 + "," + start + "," + end + ",Right,,0,0,0,," + events.Item3 + "\n";
|
||||
//Second layer, for the left eye
|
||||
line += "Dialogue: " + 1 + "," + start + "," + end + ",Left,,0,0,0,," + events.Item3;
|
||||
|
||||
endResult += line + "\n";
|
||||
}
|
||||
return endResult;
|
||||
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
|
||||
//Change thousands of a second hunderdth second
|
||||
tmp = tmp.Substring(0, tmp.Length - 1);//TODO: Do actual rounding
|
||||
return tmp.Replace(",", ".");
|
||||
}
|
||||
|
||||
//TODO: Make this human readable
|
||||
//TODO: Add adjustable parameters
|
||||
private static string CreateStandardStyle()
|
||||
{
|
||||
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: " +
|
||||
"Right," +
|
||||
"Arial," +
|
||||
"16," +
|
||||
"&Hffffff," +
|
||||
"&Hffffff," +
|
||||
"&H0," +
|
||||
"&H0," +
|
||||
"0," +
|
||||
"0," +
|
||||
"0," +
|
||||
"0," +
|
||||
"100," +
|
||||
"100," +
|
||||
"0," +
|
||||
"0," +
|
||||
"1," +
|
||||
"1," +
|
||||
"0," +
|
||||
"2," +
|
||||
"192," +
|
||||
"0," +
|
||||
"10," +
|
||||
"0\n" +
|
||||
|
||||
|
||||
"Style: " +
|
||||
"Left," +
|
||||
"Arial," +
|
||||
"16," +
|
||||
"&Hffffff," +
|
||||
"&Hffffff," +
|
||||
"&H0," +
|
||||
"&H0," +
|
||||
"0," +
|
||||
"0," +
|
||||
"0," +
|
||||
"0," +
|
||||
"100," +
|
||||
"100," +
|
||||
"0," +
|
||||
"0," +
|
||||
"1," +
|
||||
"1," +
|
||||
"0," +
|
||||
"2," +
|
||||
"0," +
|
||||
"192 ," +
|
||||
"10," +
|
||||
"0";
|
||||
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)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(file);
|
||||
string scriptinfo = "; Generated by ConvertSRTto3D\n" +
|
||||
"Title: " + name + "\n" +
|
||||
"ScriptType: v4.00+\n" +
|
||||
"Collisions: Normal\n" +
|
||||
"PlayResX: " + resX + "\n" +
|
||||
"PlayResY: " + resY + "\n" +
|
||||
"ScaledBorderAndShadow: yes";
|
||||
return scriptinfo;
|
||||
}
|
||||
|
||||
//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)
|
||||
{
|
||||
|
||||
var converted = new List<Tuple<string, string, string, string>>();
|
||||
|
||||
var timestamp_start = "";
|
||||
var timestamp_end = "";
|
||||
var subtitiles = "";
|
||||
string srt = File.ReadAllText(file);
|
||||
|
||||
//Which dialog we are on (sanity check)
|
||||
int i = 1;
|
||||
|
||||
//Whether to extract the time stamp or not
|
||||
int j = 0; //TODO: Change to a bool
|
||||
|
||||
int linecounter = 0; //Where we are in the .srt, meant for debugging
|
||||
|
||||
foreach (string line in srt.Split('\n'))
|
||||
{
|
||||
linecounter++;
|
||||
//Empty line assumes that the next line with event number thus previous dialog is finished and can be saved
|
||||
if (line == "" | line == "\r")
|
||||
{
|
||||
j = 0;
|
||||
converted.Add(new Tuple<string, string, string, string>(timestamp_start, timestamp_end, ChangeFormatting(subtitiles), ""));
|
||||
subtitiles = "";
|
||||
continue;
|
||||
}
|
||||
//Try to parse the event/dialog number
|
||||
if (int.TryParse(line, out int k))
|
||||
{
|
||||
//Event number doesn't match counted event number. Mismatch means something probably went wrong
|
||||
if (k != i)
|
||||
{
|
||||
Console.Error.WriteLine("Something went wrong");
|
||||
Console.Error.WriteLine("Something wrong on line:" + linecounter);
|
||||
System.Environment.Exit(-1);
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
//If it a timestamp is expected, extract it
|
||||
if (j == 1)
|
||||
{
|
||||
timestamp_start = line.Substring(0, 12);
|
||||
timestamp_end = line.Substring(17, 12);
|
||||
j++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (subtitiles == "")
|
||||
{
|
||||
subtitiles = line;
|
||||
subtitiles.Replace("\\.r", "");
|
||||
}
|
||||
else
|
||||
{
|
||||
subtitiles = subtitiles + "\\n" + line;
|
||||
}
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 imrayya
|
||||
Copyright (c) 2021-2026 imrayya
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,33 +1,112 @@
|
||||
# SRT subtitles to 3D subtitles
|
||||
## The problem
|
||||
I wanted watch a movie with subtitle, like I always do, but the subtitles for the movie I have was traditional .srt while the movie I was watching was a 3D movie which was **H**alf **S**ide **B**y **S**ide (HSBS). The subtitles in question were centered which was very disorienting to read while watching the movie. The solution? Write my own program to convert it for me.
|
||||
# SRT to 3D Subtitles
|
||||
|
||||
## Convert
|
||||
Currently just a command line program to convert a .srt subtitle file to an .ass subtitle file while making it compatible with the HSBS format. Either drag the SRT file on the .exe to convert the .srt file or run the command `SRTto3D.exe [insert your file name here]` and the program will convert it. Word of warning, the program will strip off any positional data off the srt. It will also likely fail with .srt file that contain a positional information.
|
||||
Convert standard `.srt` subtitles into `.ass` format, offset so they display correctly in 3D video playback.
|
||||
|
||||
## The Problem
|
||||
|
||||
When watching 3D movies (Side-by-Side, Over-Under, Anaglyph), centered subtitles get split between the left and right eye views, making them disorienting to read. This tool converts standard `.srt` subtitles into a 3D-compatible `.ass` format that keeps each eye's subtitles in the correct position.
|
||||
|
||||
## Features
|
||||
_Basically nothing right now_
|
||||
- [x] Convert a .srt to an .ass/.ssa
|
||||
- [x] For HSBS media
|
||||
- [ ] For **S**ide **B**y **S**ide (SBS) media
|
||||
- [ ] For **O**ver **U**nder (OU) media
|
||||
- [ ] For Anaglyph 3D media (red and green media)
|
||||
- [ ] For traditionally media (pancake mode).
|
||||
- _I suggest to just use ffmpeg for this use case_
|
||||
- [x] Convert formatting of .srt subtitles to transfer to the converted version
|
||||
- [ ] Convert positional data of .srt subtitles
|
||||
- [ ] Add changes which allows custom:
|
||||
- [ ] Font
|
||||
- [ ] Color
|
||||
- [ ] Position
|
||||
- [ ] Margins
|
||||
- [ ] Encoding
|
||||
- [ ] Add a proper GUI
|
||||
- [ ] Any form of error handling or unit testing
|
||||
|
||||
- **SBS (Side-by-Side)** — subtitles split left/right for each eye
|
||||
- **OU (Over-Under)** — subtitles split top/bottom for each eye
|
||||
- **Anaglyph (Red/Green)** — subtitles offset with red/green color channels
|
||||
- **CLI** — full command-line interface with configurable options
|
||||
- **GUI** — lightweight Windows interface for easier mode selection
|
||||
- **Formatting support** — converts HTML tags (`<b>`, `<i>`, `<u>`, `<font color>`) to ASS format codes
|
||||
- **Resolution scaling** — adjusts subtitle positioning based on target resolution
|
||||
|
||||
## Usage
|
||||
|
||||
The application runs in two modes — **GUI** (no arguments) and **CLI** (with arguments).
|
||||
|
||||
### GUI
|
||||
|
||||
1. Launch `ConvertSRTto3DASS.exe` (or the Linux/macOS binary)
|
||||
2. Select your `.srt` input file
|
||||
3. Choose your 3D mode (SBS, OU, or Anaglyph)
|
||||
4. Adjust settings as needed
|
||||
5. Click **Convert**
|
||||
|
||||
### Command Line
|
||||
|
||||
Pass any argument to trigger CLI mode:
|
||||
|
||||
```
|
||||
ConvertSRTto3DASS.exe input.srt [options]
|
||||
```
|
||||
|
||||
Or drag and drop an `.srt` file onto the executable.
|
||||
|
||||
Available options:
|
||||
|
||||
```
|
||||
--mode sbs|ou|rg 3D mode (default: sbs)
|
||||
--resx <number> Output width
|
||||
--resy <number> Output height
|
||||
--baseresx <number> Scaling reference width (default: 1280)
|
||||
--baseresy <number> Scaling reference height (default: 720)
|
||||
--fontsize <number> Font size (default: 16)
|
||||
--offsetx <number> RG eye separation
|
||||
--bottomoffset <number> RG bottom offset
|
||||
--sbssidemargin <number> SBS side margin
|
||||
--outopmargin <number> OU top subtitle margin
|
||||
--verticalmargin <number> General vertical margin
|
||||
--output <path> Output file path
|
||||
--help, -h, /? Show help
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
ConvertSRTto3DASS.exe movie.srt --mode sbs
|
||||
ConvertSRTto3DASS.exe movie.srt --mode ou --resx 1920 --resy 1080
|
||||
ConvertSRTto3DASS.exe movie.srt --mode rg --offsetx 6 --bottomoffset 24
|
||||
```
|
||||
|
||||
> **Note:** The GUI is experimental. Feedback and bug reports are welcome!
|
||||
|
||||
## Known Issues
|
||||
For some reason that I have yet to understand, Plex Transcoder doesn't not like the converted subtitles (it has a weird issue) but it works perfectly in VLC
|
||||
|
||||
## Warning
|
||||
The code is a complete and utter mess with very little comments in it. This was meant to be a private project and I haven't had the chance to clean it up yet.
|
||||
- Plex Transcoder has issues with the converted subtitles. VLC works correctly.
|
||||
- Subtitle positional data from `.srt` files is stripped during conversion.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **.NET 8 SDK** — required to build
|
||||
- **Windows** — the GUI runs on Windows
|
||||
- **Linux/macOS** — the CLI runs cross-platform
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Build all projects
|
||||
dotnet build
|
||||
|
||||
# Publish CLI (cross-platform)
|
||||
dotnet publish ConvertSRTto3DASS.Cli -c Release -r linux-x64 --self-contained false -p:PublishSingleFile=true
|
||||
dotnet publish ConvertSRTto3DASS.Cli -c Release -r osx-x64 --self-contained false -p:PublishSingleFile=true
|
||||
dotnet publish ConvertSRTto3DASS.Cli -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true
|
||||
|
||||
# Publish GUI (Windows only)
|
||||
dotnet publish ConvertSRTto3DASS.Gui -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License — Copyright © imrayya 2021-2026
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. Please open an issue or submit a pull request.
|
||||
|
||||
### Contributors
|
||||
|
||||
- [imrayya](https://github.com/imrayya) — project owner
|
||||
- [jae0815](https://github.com/jae0815)
|
||||
- [fabio-noga](https://github.com/fabio-noga)
|
||||
- [kaiju466](https://github.com/kaiju466)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This is a personal hobby project. The codebase is functional but not production-grade — minimal comments, no tests, and some rough edges. Use at your own discretion.
|
||||
|
||||
Reference in New Issue
Block a user