19 Commits

Author SHA1 Message Date
Kareem Horstink 1c6848bb9e ci: add architecture suffix to binary names 2026-07-01 08:27:11 +00:00
Kareem Horstink ad7860161a chore: explicitly set Core project as Library output type 2026-07-01 08:17:32 +00:00
Kareem Horstink fa679e441e ci: add rolling draft release workflow 2026-07-01 08:07:35 +00:00
Kareem Horstink 8bd53587d1 add publish scripts (PowerShell + bash) for single-command cross-platform builds 2026-07-01 06:05:16 +00:00
Imrayya 817b476c5f Update known issues in README
Remove mention of Plex Transcoder subtitle issues.
2026-06-26 00:32:35 +07:00
Imrayya 02176d0d7e Revise instructions in README
Updated CLI and GUI publishing instructions for self-contained builds on various platforms
Updated the CLI and GUI usage guide
2026-06-26 00:28:03 +07:00
Kareem Horstink 1c5d2e3934 fix: wire up Convert button Click handler (was never attached) 2026-06-25 17:04:40 +00:00
Kareem Horstink 9b7b8df874 fix: wire up GUI browse buttons, make text boxes editable, pause CLI on error, register codepage provider
GUI:
- Attach Click event handlers to browse input/output buttons (were defined but never wired up)
- Remove ReadOnly=true from input/output text boxes so users can type or copy paths

CLI:
- Make Cli.Run return bool to signal errors
- Only pause with Console.ReadLine() when an error occurs (not on success)
- Fix default case to return true

Core:
- Add System.Text.Encoding.CodePages package for CP1252 SRT file support
- Call Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) before reading files
2026-06-25 16:46:05 +00:00
Imrayya 40ce11d068 feat: modernize to .NET 8, split into Core/Cli/Gui (#3)
* feat: modernize to .NET 8, split into Core/Cli/Gui projects

Core: shared converter library (cross-platform)

Cli: console app with CLI args (cross-platform)

Gui: WinForms app (Windows only, uses Core library)

- Split into 3 projects sharing ConvertSRTto3DASS.Core

- Rewrite SrtConverter with clean public API

- Add CLI entry point with stdlib argument parsing

- Redesign GUI with GroupBox layout, flat buttons, modern fonts

- Update README with publish commands

* feat: add drag-and-drop to GUI

* refactor: rename GUI controls to proper PascalCase identifiers

---------

Co-authored-by: imrayya <imrayya@users.noreply.github.com>
2026-06-24 18:02:05 +07:00
imrayya 743900f2f7 chore: update to .NET 4.8, clean ownership, rewrite README
- Target .NET Framework 4.8 (both projects + App.config)
- Remove Stanley Black & Decker references, set ownership to imrayya
- Update copyright year to 2021-2026
- Remove stale project properties (SignAssembly, RepositoryType, etc.)
- Rewrite README: professional docs, usage guide, features, contributors, disclaimer
- Update LICENSE year range
2026-06-24 09:41:44 +00:00
Imrayya fb75071c44 Merge pull request #2 from kaiju466/feature/kaiju466/add-cli-options-and-3d-mode
feat: add CLI options and support SBS/OU/Anaglyph + experimental GUI
2026-06-24 14:46:32 +07:00
Emrick, Joshua 580c0a534d Merge branch 'feature/kaiju466/add-cli-options-and-3d-mode' of https://github.com/kaiju466/SRTto3Dsubtitles into feature/kaiju466/add-cli-options-and-3d-mode 2026-06-15 10:00:27 -04:00
kaiju466 ea0574ffb2 Update README.md 2026-06-15 09:56:57 -04:00
Emrick, Joshua 5e4e907a2c Add resolution-aware subtitle scaling and configurable layout defaults
- replace fixed RG subtitle bottom positioning with resolution-scaled Y offset
- add centralized ScaleX, ScaleY, and ScaleFont helpers
- scale RG horizontal eye offset based on output width
- scale font size, SBS margins, and OU margins based on output/base resolution
- change default baseline behavior from legacy 384x288 to tunable base resolution
- add BaseResX/BaseResY options with 1280x720 defaults
- add CLI options for:
  - --baseresx
  - --baseresy
  - --bottomoffset
  - --sbssidemargin
  - --outopmargin
  - --verticalmargin
- replace remaining layout magic numbers with configurable options:
  - BottomOffset
  - SbsSideMargin
  - OuTopMargin
  - VerticalMargin
  - FontSize
  - OffsetX
- preserve original numeric defaults for layout settings when not specified (based off of my personal experiences and preferences.
- update WinForms designer to expose new converter settings
- update WinForms code-behind to pass new options to Converter.Main
- prefill GUI controls with converter-aligned default values
- improve mode-based UI enable/disable behavior for SBS, OU, and RG settings
- prepare shared-defaults approach via ConverterDefaults for keeping GUI and converter defaults in sync
2026-06-15 09:50:26 -04:00
Emrick, Joshua 470e23291f feat: add red-green 3D mode and Windows Forms GUI
- add rg (red-green) anaglyph subtitle mode with configurable horizontal offset
- extend CLI mode handling to support sbs, ou, and rg
- add offsetx option for anaglyph subtitle positioning
- add Windows Forms GUI project for configuring and running conversions
- add file pickers for input/output paths in the GUI
- add GUI controls for mode, resolution, font size, and offset
- add status output and mode-aware UI behavior in the GUI
- update mode selection handling to support user-friendly display labels
2026-06-09 11:16:53 -04:00
Emrick, Joshua 327bc071aa 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.
2026-06-09 09:34:24 -04:00
Imrayya 9d51c5f554 Merge pull request #1 from fabio-noga/utf8
Add UTF-8 support and small fix
2025-10-19 13:16:15 +02:00
Fábio Nogueira b3f9bfb074 Fix dialog line when it's a number & Fix new line
Fix dialog line when the dialog is just a number | Fix new line in dialog that happens on the same line
2025-10-16 18:51:28 +01:00
Fábio Nogueira 28f9e4f8f9 Add UTF-8 support and small fix
that j==1 comes from an issue found while using this where a subtitle would be just a number (for example "2025") and it would pass the int.TryParse(line, out int k) check. With j==1 it always knows that it should be a timestamp
2025-10-13 20:31:58 +01:00
23 changed files with 1542 additions and 358 deletions
+78
View File
@@ -0,0 +1,78 @@
name: Build and Draft
on:
push:
branches: [main]
permissions:
contents: write
jobs:
build-and-draft:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
# ---- Build all 5 binaries (no version in filenames needed) ----
- name: Build CLI - Linux x64
run: |
dotnet publish ConvertSRTto3DASS.Cli \
-c Release -r linux-x64 --self-contained false \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true \
-o publish/linux-x64/
mv publish/linux-x64/ConvertSRTto3DASS publish/linux-x64/ConvertSRTto3DASS-cli-linux-x64
- name: Build CLI - macOS x64
run: |
dotnet publish ConvertSRTto3DASS.Cli \
-c Release -r osx-x64 --self-contained false \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true \
-o publish/osx-x64/
mv publish/osx-x64/ConvertSRTto3DASS publish/osx-x64/ConvertSRTto3DASS-cli-osx-x64
- name: Build CLI - macOS arm64
run: |
dotnet publish ConvertSRTto3DASS.Cli \
-c Release -r osx-arm64 --self-contained false \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true \
-o publish/osx-arm64/
mv publish/osx-arm64/ConvertSRTto3DASS publish/osx-arm64/ConvertSRTto3DASS-cli-osx-arm64
- name: Build CLI - Windows x64
run: |
dotnet publish ConvertSRTto3DASS.Cli \
-c Release -r win-x64 --self-contained false \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true \
-o publish/win-x64/
mv publish/win-x64/ConvertSRTto3DASS.exe publish/win-x64/ConvertSRTto3DASS-cli-win-x64.exe
- name: Build GUI - Windows x64
run: |
dotnet publish ConvertSRTto3DASS.Gui \
-c Release -r win-x64 --self-contained false \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true \
-p:EnableWindowsTargeting=true \
-o publish/gui-win-x64/
mv publish/gui-win-x64/ConvertSRTto3DASS.exe publish/gui-win-x64/ConvertSRTto3DASS-gui-win-x64.exe
# ---- Update (or create) the rolling draft release ----
- name: Update Rolling Draft Release
uses: ncipollo/release-action@v1
with:
allowUpdates: true
draft: true
tag: rolling-draft
name: "Draft (latest build)"
body: "Latest binaries from commit ${{ github.sha }}. Edit tag, name, and notes before publishing."
artifacts: |
publish/linux-x64/ConvertSRTto3DASS-cli-linux-x64
publish/osx-x64/ConvertSRTto3DASS-cli-osx-x64
publish/osx-arm64/ConvertSRTto3DASS-cli-osx-arm64
publish/win-x64/ConvertSRTto3DASS-cli-win-x64.exe
publish/gui-win-x64/ConvertSRTto3DASS-gui-win-x64.exe
+153
View File
@@ -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>
+11
View File
@@ -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,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<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>
+276
View File
@@ -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>
+201
View File
@@ -0,0 +1,201 @@
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)"]);
comboBox_Mode.SelectedIndexChanged += comboBox_Mode_SelectedIndexChanged;
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 }
};
button_Convert.Click += button_Convert_Click;
// ---- 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();
}
}
+170
View File
@@ -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);
}
}
+120
View File
@@ -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>
+11
View File
@@ -0,0 +1,11 @@
namespace ConvertSRTto3DASS;
internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
}
+71
View File
@@ -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>
@@ -8,17 +8,21 @@
// </auto-generated> // </auto-generated>
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
namespace ConvertSRTto3DASS.Properties { namespace ConvertSRTto3DASS.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default { public static Settings Default
get { {
get
{
return defaultInstance; return defaultInstance;
} }
} }
@@ -3,4 +3,5 @@
<Profiles> <Profiles>
<Profile Name="(Default)" /> <Profile Name="(Default)" />
</Profiles> </Profiles>
<Settings />
</SettingsFile> </SettingsFile>
+20 -14
View File
@@ -1,9 +1,13 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 17
VisualStudioVersion = 16.0.30523.141 VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,15 +15,17 @@ Global
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7D7925DE-ADBD-4A26-B12B-8F12B68D3BFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D7925DE-ADBD-4A26-B12B-8F12B68D3BFE}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D7925DE-ADBD-4A26-B12B-8F12B68D3BFE}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D7925DE-ADBD-4A26-B12B-8F12B68D3BFE}.Release|Any CPU.Build.0 = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
GlobalSection(SolutionProperties) = preSolution {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
HideSolutionNode = FALSE {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
GlobalSection(ExtensibilityGlobals) = postSolution {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
SolutionGuid = {B5E10CFB-7432-4C4B-80FB-1B9349811932} {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 EndGlobalSection
EndGlobal 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>
-277
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2021 imrayya Copyright (c) 2021-2026 imrayya
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+119 -26
View File
@@ -1,33 +1,126 @@
# SRT subtitles to 3D subtitles # SRT 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.
## Convert Convert standard `.srt` subtitles into `.ass` format, offset so they display correctly in 3D video playback.
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.
## 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 ## 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**
> **Note:** The GUI is experimental. Feedback and bug reports are welcome!
### Command Line
The CLI runs on **Windows, Linux, and macOS**.
#### Drag and drop:
1. Drag and drop an `.srt` file onto `ConvertSRTto3DASS`
- Uses default settings (SBS mode, 1280x720)
2. The resulting `.ass` file will be saved in the same directory as the `.srt`
#### Command line:
```
ConvertSRTto3DASS.exe input.srt [options]
```
##### 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
```
## Known Issues ## 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 - Subtitle positional data from `.srt` files is stripped during conversion.
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.
## 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 (self-contained)
## Linux x64
dotnet publish ConvertSRTto3DASS.Cli -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
## OSX x64
dotnet publish ConvertSRTto3DASS.Cli -c Release -r osx-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
## OSX arm64
dotnet publish ConvertSRTto3DASS.Cli -c Release -r osx-arm64 --self-contained false -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
## Windows x64
dotnet publish ConvertSRTto3DASS.Cli -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
# Publish GUI (Windows x64 only, self-contained)
dotnet publish ConvertSRTto3DASS.Gui -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
```
_Each publish produces a single .exe with no .dll sidecars._
## 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.
+31
View File
@@ -0,0 +1,31 @@
# Publish all targets for SRTto3Dsubtitles
# Usage: ./publish.ps1
$ErrorActionPreference = "Stop"
$targets = @(
@{ Project = "ConvertSRTto3DASS.Cli"; Runtime = "linux-x64"; Name = "ConvertSRTto3DASS-linux-x64" },
@{ Project = "ConvertSRTto3DASS.Cli"; Runtime = "win-x64"; Name = "ConvertSRTto3DASS-win-x64.exe" },
@{ Project = "ConvertSRTto3DASS.Cli"; Runtime = "osx-x64"; Name = "ConvertSRTto3DASS-osx-x64" },
@{ Project = "ConvertSRTto3DASS.Cli"; Runtime = "osx-arm64"; Name = "ConvertSRTto3DASS-osx-arm64" },
@{ Project = "ConvertSRTto3DASS.Gui"; Runtime = "win-x64"; Name = "ConvertSRTto3DASS-GUI-Win-x64.exe" }
)
foreach ($t in $targets) {
Write-Host "Publishing $($t.Name)..." -ForegroundColor Cyan
dotnet publish $t.Project -c Release -r $t.Runtime `
--self-contained false `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:OutputType=Exe `
-p:OutputPath="publish\$($t.Runtime)\"
$src = "publish\$($t.Runtime)\$($t.Project).exe"
$dst = "publish\$($t.Name)"
if (Test-Path $src) {
Move-Item $src $dst -Force
Write-Host " -> $dst" -ForegroundColor Green
}
}
Write-Host "`nDone! Published files in publish/`n" -ForegroundColor Green
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Publish all targets for SRTto3Dsubtitles
# Usage: ./publish.sh
set -e
declare -a targets=(
"ConvertSRTto3DASS.Cli:linux-x64:ConvertSRTto3DASS-linux-x64"
"ConvertSRTto3DASS.Cli:win-x64:ConvertSRTto3DASS-win-x64.exe"
"ConvertSRTto3DASS.Cli:osx-x64:ConvertSRTto3DASS-osx-x64"
"ConvertSRTto3DASS.Cli:osx-arm64:ConvertSRTto3DASS-osx-arm64"
"ConvertSRTto3DASS.Gui:win-x64:ConvertSRTto3DASS-GUI-Win-x64.exe"
)
rm -rf publish
mkdir -p publish
for entry in "${targets[@]}"; do
IFS=':' read -r project runtime name <<< "$entry"
echo "Publishing $name..."
dotnet publish "$project" -c Release -r "$runtime" \
--self-contained false \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:OutputType=Exe \
-p:OutputPath="publish/$runtime/"
src="publish/$runtime/$project.exe"
dst="publish/$name"
if [ -f "$src" ]; then
mv "$src" "$dst"
echo " -> $dst"
fi
done
echo ""
echo "Done! Published files in publish/"