diff --git a/ASSLoader.AutoEffects/ASSLoader.AutoEffects.csproj b/ASSLoader.AutoEffects/ASSLoader.AutoEffects.csproj index c517ce3..e7f8964 100644 --- a/ASSLoader.AutoEffects/ASSLoader.AutoEffects.csproj +++ b/ASSLoader.AutoEffects/ASSLoader.AutoEffects.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.1 diff --git a/ASSLoader.NET/ASSEvent.cs b/ASSLoader.NET/ASSEvent.cs new file mode 100644 index 0000000..ec40a34 --- /dev/null +++ b/ASSLoader.NET/ASSEvent.cs @@ -0,0 +1,155 @@ +using ASSLoader.NET.Enums; +using log4net; +using System; +using System.Collections.Generic; +using System.Text; + +namespace ASSLoader.NET +{ + public class ASSEvent : ICloneable + { + public ASSEventType Type { get; set; } + + public int Layer { get; set; } + + public ASSEventTime Start { get; set; } + + public ASSEventTime End { get; set; } + + public string Style { get; set; } + + public string Name { get; set; } + + public int MarginL { get; set; } + + public int MarginR { get; set; } + + public int MarginV { get; set; } + + public string Effect { get; set; } + + public string Text { get; set; } + + private static ILog log = LogManager.GetLogger(typeof(ASSStyle)); + + public static IList DefaultFormat = new List { + "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect", "Text" + }; + + public object Clone() + { + var newInst = new ASSEvent(); + newInst.Type = this.Type; + newInst.Layer = this.Layer; + newInst.Start = new ASSEventTime(this.Start.ToString()); + newInst.End = new ASSEventTime(this.End.ToString()); + newInst.Style = this.Style; + newInst.Name = this.Name; + newInst.MarginL = this.MarginL; + newInst.MarginR = this.MarginR; + newInst.MarginV = this.MarginV; + newInst.Effect = this.Effect; + newInst.Text = this.Text; + return newInst; + } + + public ASSEvent() + { + this.Type = ASSEventType.Dialogue; + this.Layer = 0; + this.Start = new ASSEventTime(0, 0, 0, 0); + this.End = new ASSEventTime(0, 0, 0, 0); + this.Style = "Default"; + this.Name = string.Empty; + this.MarginL = 0; + this.MarginR = 0; + this.MarginV = 0; + this.Effect = string.Empty; + this.Text = string.Empty; + } + + + /// + /// Convert an event text into ASSEvent object. + /// + /// The prefix of the line of the event text. Must be one of the ASSEventType. + /// The headings list from the "Format" of the "Events" section. + /// The values list of the line of the event text. + /// The ASSEvent object. + public static ASSEvent Parse(string prefix, IList eventFormat, IList values) + { + var eventType = (ASSEventType)Enum.Parse(typeof(ASSEventType), prefix); + var evt = new ASSEvent(); + evt.Type = eventType; + for (var i = 0; i < eventFormat.Count; i++) + { + var field = eventFormat[i]; + var value = values[i]; + switch (field.Trim()) + { + case "Layer": evt.Layer = Convert.ToInt32(value); continue; + case "Start": evt.Start = new ASSEventTime(value); continue; + case "End": evt.End = new ASSEventTime(value); continue; + case "Style": evt.Style = value; continue; + case "Name": evt.Name = value; continue; + case "MarginL": evt.MarginL = Convert.ToInt32(value); continue; + case "MarginR": evt.MarginR = Convert.ToInt32(value); continue; + case "MarginV": evt.MarginV = Convert.ToInt32(value); continue; + case "Effect": evt.Effect = value; continue; + case "Text": evt.Text = value; continue; + default: + log.Warn($"EVENT MAPPING ERROR: Unknown field skipped: [{field}]."); + continue; + } + } + return evt; + } + + /// + /// Convert an ASSEvent object into a line of event text. + /// + /// The headings list from the "Format" of the "Events" section. + /// The spliter used for stringify. Defaultly use ",". + /// The event text converted from ASSEvent. + public string Stringify(IList eventFormat, string spliter = ",") + { + var sb = new StringBuilder(); + sb.Append(this.Type.ToString() + ": "); + for (var i = 0; i < eventFormat.Count; i++) + { + var field = eventFormat[i]; + switch (field.Trim()) + { + case "Layer": sb.Append(this.Layer); break; + case "Start": sb.Append(this.Start); break; + case "End": sb.Append(this.End); break; + case "Style": sb.Append(this.Style); break; + case "Name": sb.Append(this.Name); break; + case "MarginL": sb.Append(this.MarginL); break; + case "MarginR": sb.Append(this.MarginR); break; + case "MarginV": sb.Append(this.MarginV); break; + case "Effect": sb.Append(this.Effect); break; + case "Text": sb.Append(this.Text); break; + default: + log.Warn($"EVENT MAPPING ERROR: Unknown field skipped: [{field}]."); + break; + } + if (i != eventFormat.Count - 1) + { + sb.Append(spliter); + } + } + return sb.ToString(); + } + + /// + /// Only for debug usage. + /// + /// + public override string ToString() + { + return $"{Start} - {End} | {Type}:{Name}:{Text}"; + } + } + +} diff --git a/ASSLoader.NET/ASSEventTime.cs b/ASSLoader.NET/ASSEventTime.cs new file mode 100644 index 0000000..35a60f6 --- /dev/null +++ b/ASSLoader.NET/ASSEventTime.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ASSLoader.NET +{ + public class ASSEventTime : IComparable + { + public int Hour { get; set; } + + public int Minute { get; set; } + + public int Second { get; set; } + + public int Millisecond { get; set; } + + public ASSEventTime(string assTime) + { + var parts = assTime.Split(':', '.'); + var msIndex = parts.Length - 1; + var secIndex = parts.Length - 2; + var minIndex = parts.Length - 3; + var hourIndex = parts.Length - 4; + this.Hour = hourIndex >= 0 ? Convert.ToInt32(parts[hourIndex]) : 0; + this.Minute = minIndex >= 0 ? Convert.ToInt32(parts[minIndex]) : 0; + this.Second = secIndex >= 0 ? Convert.ToInt32(parts[secIndex]) : 0; + this.Millisecond = msIndex >= 0 ? Convert.ToInt32((parts[msIndex] + "000").Substring(0, 3)) : 0; + } + + public ASSEventTime(int hour, int minute, int second, int millisecond) + { + this.Hour = hour; + this.Minute = minute; + this.Second = second; + this.Millisecond = millisecond; + } + + public long TotalMilliseconds() + { + return this.Hour * 3600000 + this.Minute * 60000 + this.Second * 1000 + this.Millisecond; + } + + public static explicit operator ASSEventTime(TimeSpan ts) + { + return new ASSEventTime(ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds); + } + + public static explicit operator TimeSpan(ASSEventTime time) + { + return new TimeSpan(0, time.Hour, time.Minute, time.Second, time.Millisecond); + } + + public static ASSEventTime operator +(ASSEventTime aet, double num) + { + var target = new ASSEventTime(aet.ToString()); + var ms = Convert.ToInt32(Math.Floor(num * 1000)); + target.Millisecond = target.Millisecond + ms; + if (target.Millisecond > 1000) + { + target.Second += target.Millisecond / 1000; + target.Millisecond = target.Millisecond % 1000; + } + if (target.Second > 60) + { + target.Minute += target.Second / 60; + target.Second = target.Second % 60; + } + if (target.Minute > 60) + { + target.Hour += target.Minute / 60; + target.Minute = target.Minute % 60; + } + + return target; + } + + public static ASSEventTime operator -(ASSEventTime aet, double num) + { + var ms = Convert.ToInt32(Math.Floor(num * 1000)); + var target = new ASSEventTime(aet.ToString()); + target.Millisecond = aet.Millisecond - ms; + if (target.Millisecond < 0) + { + target.Millisecond += 1000; + target.Second -= 1; + } + if (target.Second < 0) + { + target.Second += 60; + target.Minute -= 1; + } + if (target.Minute < 0) + { + target.Minute += 60; + target.Hour -= 1; + } + return target; + } + + public override bool Equals(object obj) + { + return CompareTo(obj) == 0; + } + + public override int GetHashCode() + { + return (int)this.TotalMilliseconds(); + } + + public override string ToString() + { + return this.Hour.ToString().Substring(0, 1) + ":" + + this.Minute.ToString().PadLeft(2, '0') + ":" + + this.Second.ToString().PadLeft(2, '0') + "." + + this.Millisecond.ToString().PadLeft(3, '0').Substring(0, 2); + } + + public int CompareTo(object obj) + { + var target = obj as ASSEventTime; + if (target == null) + { + target = new ASSEventTime(obj.ToString()); + } + return (int)(this.TotalMilliseconds() - target.TotalMilliseconds()); + } + } +} diff --git a/ASSLoader.NET/ASSLoader.NET.csproj b/ASSLoader.NET/ASSLoader.NET.csproj index 91893c8..35c703a 100644 --- a/ASSLoader.NET/ASSLoader.NET.csproj +++ b/ASSLoader.NET/ASSLoader.NET.csproj @@ -1,13 +1,19 @@ - netstandard2.0 + netstandard2.1;net45 true - 0.0.1 + 0.9.3.3-pre Akaishi Toshiya - RMEGo Studio + Shiba Studio A ass subtitle toolkit. https://github.com/toshiya14/ASSLoader.NET - + Akaishi Toshiya + true + + + + + diff --git a/ASSLoader.NET/ASSStyle.cs b/ASSLoader.NET/ASSStyle.cs new file mode 100644 index 0000000..c154482 --- /dev/null +++ b/ASSLoader.NET/ASSStyle.cs @@ -0,0 +1,190 @@ +using ASSLoader.NET.Enums; +using log4net; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace ASSLoader.NET +{ + + public class ASSStyle + { + public string Name { get; set; } + + public string Fontname { get; set; } + + public double Fontsize { get; set; } + + public string PrimaryColour { get; set; } + + public string SecondaryColour { get; set; } + + public string OutlineColour { get; set; } + + public string BackColour { get; set; } + + public bool Bold { get; set; } + + public bool Italic { get; set; } + + public bool Underline { get; set; } + + public bool StrikeOut { get; set; } + + public double ScaleX { get; set; } + + public double ScaleY { get; set; } + + public double Spacing { get; set; } + + public double Angle { get; set; } + + public V4pStyleBorderStyle BorderStyle { get; set; } + + public double Outline { get; set; } + + public double Shadow { get; set; } + + public V4pStyleAlignment Alignment { get; set; } + + public int MarginL { get; set; } + + public int MarginR { get; set; } + + public int MarginV { get; set; } + + public int AlphaLevel { get; set; } + + public int Encoding { get; set; } + + private static ILog log = LogManager.GetLogger(typeof(ASSStyle)); + + public static readonly IList DefaultFormat = new List { + "Name", "Fontname", "Fontsize", "PrimaryColour", "SecondaryColour", "OutlineColour", "BackColour", "Bold", "Italic", "Underline", "StrikeOut", "ScaleX", "ScaleY", "Spacing", "Angle", "BorderStyle", "Outline", "Shadow", "Alignment", "MarginL", "MarginR", "MarginV", "Encoding" + }; + + /// + /// Convert an ASSStle object into a line of style text. + /// + /// The headings list from the "Format" of the "Styles" section. + /// The ASSStyle object. + /// The spliter used for stringify. Defaultly use ",". + /// The style text converted from ASSEvent. + public string Stringify(IList v4pStyleFormat, string spliter = ",") + { + var sb = new StringBuilder(); + var fp = CultureInfo.InvariantCulture; + sb.Append("Style: "); + for (var i = 0; i < v4pStyleFormat.Count; i++) + { + var field = v4pStyleFormat[i]; + switch (field.Trim()) + { + case "Name": sb.Append(this.Name); break; + case "Fontname": sb.Append(this.Fontname); break; + case "Fontsize": sb.Append(this.Fontsize.ToString(fp)); break; + case "PrimaryColour": sb.Append(this.PrimaryColour); break; + case "SecondaryColour": sb.Append(this.SecondaryColour); break; + case "OutlineColour": sb.Append(this.OutlineColour); break; + case "BackColour": sb.Append(this.BackColour); break; + case "Bold": sb.Append(this.Bold ? "-1" : "0"); break; + case "Italic": sb.Append(this.Italic ? "-1" : "0"); break; + case "Underline": sb.Append(this.Underline ? "-1" : "0"); break; + case "StrikeOut": sb.Append(this.StrikeOut ? "-1" : "0"); break; + case "ScaleX": sb.Append(this.ScaleX.ToString(fp)); break; + case "ScaleY": sb.Append(this.ScaleY.ToString(fp)); break; + case "Spacing": sb.Append(this.Spacing.ToString(fp)); break; + case "Angle": sb.Append(this.Angle.ToString(fp)); break; + case "BorderStyle": sb.Append((int)this.BorderStyle); break; + case "Outline": sb.Append(this.Outline.ToString(fp)); break; + case "Shadow": sb.Append(this.Shadow.ToString(fp)); break; + case "Alignment": sb.Append((int)this.Alignment); break; + case "MarginL": sb.Append(this.MarginL); break; + case "MarginR": sb.Append(this.MarginR); break; + case "MarginV": sb.Append(this.MarginV); break; + case "Encoding": sb.Append(this.Encoding); break; + default: + log.Warn($"STYLE MAPPING ERROR: Unknown field skipped: [{field}]."); + break; + } + if (i != v4pStyleFormat.Count - 1) + { + sb.Append(spliter); + } + } + return sb.ToString(); + } + + private static bool ParseASSBoolean(int value) + { + if (value == -1) + { + return true; + } + else if(value==0){ + return false; + } + else + { + throw new ArgumentException($"The boolean value in .ass file must be '-1(true)' or '0(false)', but {value} got."); + } + } + + /// + /// Convert a style text into ASSStyle object. + /// + /// The headings list from the "Format" of the "Styles" section. + /// + /// If the boolean value is not '-1' or '0', it would throw ArgumentException. + /// + public static ASSStyle Parse(IList v4pStyleFormat, IList values) + { + var style = new ASSStyle(); + for (var i = 0; i < v4pStyleFormat.Count; i++) + { + var field = v4pStyleFormat[i]; + var value = values[i]; + switch (field.Trim()) + { + case "Name": style.Name = value; continue; + case "Fontname": style.Fontname = value; continue; + case "Fontsize": style.Fontsize = Convert.ToDouble(value); continue; + case "PrimaryColour": style.PrimaryColour = value; continue; + case "SecondaryColour": style.SecondaryColour = value; continue; + case "OutlineColour": style.OutlineColour = value; continue; + case "BackColour": style.BackColour = value; continue; + case "Bold": style.Bold = ParseASSBoolean(Convert.ToInt16(value)); continue; + case "Italic": style.Italic = ParseASSBoolean(Convert.ToInt16(value)); continue; + case "Underline": style.Underline = ParseASSBoolean(Convert.ToInt16(value)); continue; + case "StrikeOut": style.StrikeOut = ParseASSBoolean(Convert.ToInt16(value)); continue; + case "ScaleX": style.ScaleX = Convert.ToDouble(value); continue; + case "ScaleY": style.ScaleY = Convert.ToDouble(value); continue; + case "Spacing": style.Spacing = Convert.ToDouble(value); continue; + case "Angle": style.Angle = Convert.ToDouble(value); continue; + case "BorderStyle": style.BorderStyle = (V4pStyleBorderStyle)Convert.ToInt16(value); continue; + case "Outline": style.Outline = Convert.ToDouble(value); continue; + case "Shadow": style.Shadow = Convert.ToDouble(value); continue; + case "Alignment": style.Alignment = (V4pStyleAlignment)Convert.ToInt16(value); continue; + case "MarginL": style.MarginL = Convert.ToInt32(value); continue; + case "MarginR": style.MarginR = Convert.ToInt32(value); continue; + case "MarginV": style.MarginV = Convert.ToInt32(value); continue; + case "Encoding": style.Encoding = Convert.ToInt32(value); continue; + default: + log.Warn($"STYLE MAPPING ERROR: Unknown field skipped: [{field}]."); + continue; + } + } + return style; + } + + /// + /// For debug output usage. + /// + /// + public override string ToString() + { + return $"{Name}: {Fontname},{Fontsize}{(Bold ? "B" : "")}{(Italic ? "I" : "")}{(Underline ? "U" : "")}{(StrikeOut ? "S" : "")}"; + } + } +} diff --git a/ASSLoader.NET/ASSSubtitle.cs b/ASSLoader.NET/ASSSubtitle.cs index 85d6977..d68da3a 100644 --- a/ASSLoader.NET/ASSSubtitle.cs +++ b/ASSLoader.NET/ASSSubtitle.cs @@ -1,3 +1,4 @@ +using ASSLoader.NET.Enums; using ASSLoader.NET.Exceptions; using System; using System.Collections.Generic; @@ -9,19 +10,64 @@ namespace ASSLoader.NET { + public struct ASSScriptInfo + { + public ScriptInfoType Type; + public string Key; + public string Value; + + // implicit converted from tuple to keep compatibility with old version. + public static implicit operator ASSScriptInfo(Tuple tuple) + { + var si = new ASSScriptInfo(); + switch (tuple.Item1.ToLower()) + { + case "comment": + si.Type = ScriptInfoType.Comments; + si.Value = tuple.Item2; + break; + + case "key-value": + si.Type = ScriptInfoType.KeyValue; + break; + + default: + si.Type = ScriptInfoType.Unspecified; + break; + } + si.Value = tuple.Item2; + return si; + } + } public class ASSSubtitle { - public const bool showAbstract = true; - public Dictionary> ScriptInfo { get; set; } + public Dictionary ScriptInfo { get; set; } + public IList V4pStyleFormat { get; set; } + public Dictionary V4pStyles { get; set; } + public IList EventFormat { get; set; } + public IList Events { get; set; } + + /// + /// *** NOT IMPLEMENT FOR NOW. + /// public Dictionary Fonts { get; set; } + + /// + /// *** NOT IMPLEMENT FOR NOW. + /// public Dictionary Graphics { get; set; } public Dictionary UnknownSections { get; set; } + /// + /// Load a ".ass" file, and generate the ASSSubtitle object. + /// + /// The path of the ".ass" file. + /// The encoding used to process the file. Default encoding is 'utf-8'(no BOM). public void Load(string path, Encoding enc = null) { string[] lines; @@ -36,10 +82,11 @@ public void Load(string path, Encoding enc = null) var workingSection = ASSSection.ScriptInfo; var scriptInfoCommentIndex = 0; + var unknowSectionName = string.Empty; + V4pStyleFormat = new List(); EventFormat = new List(); - - ScriptInfo = new Dictionary>(); + ScriptInfo = new Dictionary(); V4pStyles = new Dictionary(); Events = new List(); Fonts = new Dictionary(); @@ -48,7 +95,8 @@ public void Load(string path, Encoding enc = null) // Regex defination var regexScriptInfoKeyValue = new Regex(@"^(?[0-9a-zA-z ]+)\s*\:\s*(?.+)$"); - var unknowSectionName = string.Empty; + var availablePrefix = Enum.GetNames(typeof(ASSEventType)); + var regexAvailablePrefix = new Regex(@"^(?" + string.Join("|", availablePrefix) + @"):\s*(?.+)$"); for (var lineIndex = 0; lineIndex < lines.Length; lineIndex++) { @@ -107,7 +155,8 @@ public void Load(string path, Encoding enc = null) switch (workingSection) { - // Skiplines + // Unknown section processor. + // Each unknown sections would keep the same while outputing. case ASSSection.Unknown: if (UnknownSections.ContainsKey(unknowSectionName)) { @@ -119,16 +168,18 @@ public void Load(string path, Encoding enc = null) } continue; - // Loading ScriptInfo section. + // [Script Info] Processor. + // Lines starts with `!:` and `;` would be skipped because they are identified as comments. + // `regexScriptInfoKeyValue` would be used for the other lines to parse the key-value pair script informations. case ASSSection.ScriptInfo: if (line.StartsWith("!:")) { - ScriptInfo["Comment" + scriptInfoCommentIndex] = new Tuple("comment", line.Substring(2)); + ScriptInfo["Comment" + scriptInfoCommentIndex] = new ASSScriptInfo { Type = ScriptInfoType.Comments, Value = line.Substring(2) }; scriptInfoCommentIndex++; } else if (line.StartsWith(";")) { - ScriptInfo["Comment" + scriptInfoCommentIndex] = new Tuple("comment", line.Substring(1)); + ScriptInfo["Comment" + scriptInfoCommentIndex] = new ASSScriptInfo { Type = ScriptInfoType.Comments, Value = line.Substring(1) }; scriptInfoCommentIndex++; } else @@ -138,7 +189,7 @@ public void Load(string path, Encoding enc = null) { var key = match.Groups["key"].Value; var value = match.Groups["value"].Value; - ScriptInfo[key] = new Tuple("key-value", value); + ScriptInfo[key] = new ASSScriptInfo { Type = ScriptInfoType.KeyValue, Key = key, Value = value }; } else { @@ -148,7 +199,15 @@ public void Load(string path, Encoding enc = null) } continue; - // Loading V4 Styles+ Section. + // [V4+Styles] Processor + // + // 1. Check the first line in this section. + // It should be started with "Format:" and followed with the headings of each column splitted by ",". + // If "Format" line was missing, the whole [V4+Styles] section would be skipped. + // + // 2. The remaining lines should be started with "Style:" and followed by the values of each column splitted by ",". + // * The count of the values in each line of these "Style" must as same as the count of headings in "Format". + // * `MappingToStyle` function could help to parse the list of values(not the whole string of the line) into ASSStyle object. case ASSSection.V4pStyles: if (V4pStyleFormat.Count == 0) { @@ -172,7 +231,7 @@ public void Load(string path, Encoding enc = null) try { // mapping - var style = MappingToStyle(V4pStyleFormat, values); + var style = ASSStyle.Parse(V4pStyleFormat, values); V4pStyles[style.Name] = style; } catch (Exception exc) @@ -195,7 +254,16 @@ public void Load(string path, Encoding enc = null) } continue; - // Loading Events Section. + // [Events] Processor + // + // 1. Check the first line in this section. + // It should be started with "Format:" and followed with the headings of each column splitted by ",". + // If "Format" line was missing, the whole [Events] section would be skipped. + // + // 2. The remaining lines should be started with one of `ASSEventType` and ":". + // Then followed by the values of each column splitted by ",". + // * The count of the values in each line of these "Event" must as same as the count of headings in "Format". + // * `MappingToEvent` function could help to parse the list of values(not the whole string of the line) into ASSEvent object. case ASSSection.Events: if (EventFormat.Count == 0) { @@ -211,9 +279,7 @@ public void Load(string path, Encoding enc = null) } else { - var availablePrefix = Enum.GetNames(typeof(ASSEventType)); - var regex = new Regex(@"^(?" + string.Join("|", availablePrefix) + @"):\s*(?.+)$"); - var match = regex.Match(line); + var match = regexAvailablePrefix.Match(line); if (match.Success) { var values = match.Groups["values"].Value.Split(new[] { ',' }, EventFormat.Count).Select(x => x.Trim()).ToList(); @@ -222,7 +288,7 @@ public void Load(string path, Encoding enc = null) try { // mapping - var evt = MappingToEvent(match.Groups["prefix"].Value, EventFormat, values); + var evt = ASSEvent.Parse(match.Groups["prefix"].Value, EventFormat, values); Events.Add(evt); } catch (Exception exc) @@ -248,27 +314,32 @@ public void Load(string path, Encoding enc = null) } } + /// + /// Convert ASSSubtitle object into ".ass" text formatted string. + /// + /// ASS formatted string. public string Stringify() { var sb = new StringBuilder(); // Script Info sb.AppendLine("[Script Info]"); - foreach (var si in ScriptInfo) + foreach (var kv in ScriptInfo) { - if (si.Value.Item1.Equals("comment")) + var si = kv.Value; + if (si.Type == ScriptInfoType.Comments) { - sb.AppendLine($";" + si.Value.Item2); + sb.AppendLine($";{si.Value}"); } - if (si.Value.Item1.Equals("key-value")) + if (si.Type == ScriptInfoType.KeyValue) { - sb.AppendLine($"{si.Key}: {si.Value.Item2}"); + sb.AppendLine($"{si.Key}: {si.Value}"); } } sb.AppendLine(); // Unknown Sections - foreach(var us in UnknownSections) + foreach (var us in UnknownSections) { sb.AppendLine(us.Key); sb.AppendLine(us.Value); @@ -280,7 +351,8 @@ public string Stringify() sb.AppendLine($"Format: {string.Join(", ", V4pStyleFormat)}"); foreach (var s in V4pStyles) { - sb.AppendLine(FormatStyle(V4pStyleFormat, s.Value)); + var style = s.Value; + sb.AppendLine(style.Stringify(V4pStyleFormat)); } sb.AppendLine(); @@ -289,13 +361,18 @@ public string Stringify() sb.AppendLine($"Format: {string.Join(", ", EventFormat)}"); foreach (var ev in Events) { - sb.AppendLine(FormatEvent(EventFormat, ev)); + sb.AppendLine(ev.Stringify(EventFormat)); } sb.AppendLine(); return sb.ToString(); } + /// + /// Convert ASSSubtitle object into ".ass" text formatted string, and then save into a file. + /// + /// The path of the ".ass" file. + /// The encoding used to process the file. Default encoding is 'utf-8'(no BOM). public void Save(string path, Encoding enc = null) { if (enc == null) @@ -308,460 +385,51 @@ public void Save(string path, Encoding enc = null) } } - private static ASSEvent MappingToEvent(string prefix, IList eventFormat, IList values) - { - var eventType = (ASSEventType)Enum.Parse(typeof(ASSEventType), prefix); - var evt = new ASSEvent(); - evt.Type = eventType; - for (var i = 0; i < eventFormat.Count; i++) - { - var field = eventFormat[i]; - var value = values[i]; - switch (field.Trim()) - { - case "Layer": evt.Layer = Convert.ToInt32(value); continue; - case "Start": evt.Start = new ASSEventTime(value); continue; - case "End": evt.End = new ASSEventTime(value); continue; - case "Style": evt.Style = value; continue; - case "Name": evt.Name = value; continue; - case "MarginL": evt.MarginL = Convert.ToInt32(value); continue; - case "MarginR": evt.MarginR = Convert.ToInt32(value); continue; - case "MarginV": evt.MarginV = Convert.ToInt32(value); continue; - case "Effect": evt.Effect = value; continue; - case "Text": evt.Text = value; continue; - default: - Trace.TraceWarning("MAPPING ERROR: Unknown field - " + field); - continue; - } - } - return evt; - } - - private static string FormatEvent(IList eventFormat, ASSEvent evt, string spliter = ",") - { - var sb = new StringBuilder(); - sb.Append(evt.Type.ToString() + ": "); - for (var i = 0; i < eventFormat.Count; i++) - { - var field = eventFormat[i]; - switch (field.Trim()) - { - case "Layer": sb.Append(evt.Layer); break; - case "Start": sb.Append(evt.Start); break; - case "End": sb.Append(evt.End); break; - case "Style": sb.Append(evt.Style); break; - case "Name": sb.Append(evt.Name); break; - case "MarginL": sb.Append(evt.MarginL); break; - case "MarginR": sb.Append(evt.MarginR); break; - case "MarginV": sb.Append(evt.MarginV); break; - case "Effect": sb.Append(evt.Effect); break; - case "Text": sb.Append(evt.Text); break; - default: - Trace.TraceWarning("MAPPING ERROR: Unknown field - " + field); - break; - } - if (i != eventFormat.Count - 1) - { - sb.Append(spliter); - } - } - return sb.ToString(); - } - - private static ASSStyle MappingToStyle(IList v4pStyleFormat, IList values) - { - var style = new ASSStyle(); - for (var i = 0; i < v4pStyleFormat.Count; i++) - { - var field = v4pStyleFormat[i]; - var value = values[i]; - switch (field.Trim()) - { - case "Name": style.Name = value; continue; - case "Fontname": style.Fontname = value; continue; - case "Fontsize": style.Fontsize = Convert.ToDouble(value); continue; - case "PrimaryColour": style.PrimaryColour = value; continue; - case "SecondaryColour": style.SecondaryColour = value; continue; - case "OutlineColour": style.OutlineColour = value; continue; - case "BackColour": style.BackColour = value; continue; - case "Bold": style.Bold = Convert.ToInt16(value) == -1; continue; - case "Italic": style.Italic = Convert.ToInt16(value) == -1; continue; - case "Underline": style.Underline = Convert.ToInt16(value) == -1; continue; - case "StrikeOut": style.StrikeOut = Convert.ToInt16(value) == -1; continue; - case "ScaleX": style.ScaleX = Convert.ToDouble(value); continue; - case "ScaleY": style.ScaleY = Convert.ToDouble(value); continue; - case "Spacing": style.Spacing = Convert.ToInt32(value); continue; - case "Angle": style.Angle = Convert.ToDouble(value); continue; - case "BorderStyle": style.BorderStyle = (V4pStyleBorderStyle)Convert.ToInt16(value); continue; - case "Outline": style.Outline = Convert.ToInt32(value); continue; - case "Shadow": style.Shadow = Convert.ToInt32(value); continue; - case "Alignment": style.Alignment = (V4pStyleAlignment)Convert.ToInt16(value); continue; - case "MarginL": style.MarginL = Convert.ToInt32(value); continue; - case "MarginR": style.MarginR = Convert.ToInt32(value); continue; - case "MarginV": style.MarginV = Convert.ToInt32(value); continue; - case "Encoding": style.Encoding = Convert.ToInt32(value); continue; - default: - Trace.TraceWarning("MAPPING ERROR: Unknown field - " + field); - continue; - } - } - return style; - } - - private static string FormatStyle(IList v4pStyleFormat, ASSStyle style, string spliter = ",") - { - var sb = new StringBuilder(); - sb.Append("Style: "); - for (var i = 0; i < v4pStyleFormat.Count; i++) - { - var field = v4pStyleFormat[i]; - switch (field.Trim()) - { - case "Name": sb.Append(style.Name); break; - case "Fontname": sb.Append(style.Fontname); break; - case "Fontsize": sb.Append(style.Fontsize); break; - case "PrimaryColour": sb.Append(style.PrimaryColour); break; - case "SecondaryColour": sb.Append(style.SecondaryColour); break; - case "OutlineColour": sb.Append(style.OutlineColour); break; - case "BackColour": sb.Append(style.BackColour); break; - case "Bold": sb.Append(style.Bold ? "-1" : "0"); break; - case "Italic": sb.Append(style.Italic ? "-1" : "0"); break; - case "Underline": sb.Append(style.Underline ? "-1" : "0"); break; - case "StrikeOut": sb.Append(style.StrikeOut ? "-1" : "0"); break; - case "ScaleX": sb.Append(style.ScaleX); break; - case "ScaleY": sb.Append(style.ScaleY); break; - case "Spacing": sb.Append(style.Spacing); break; - case "Angle": sb.Append(style.Angle); break; - case "BorderStyle": sb.Append((int)style.BorderStyle); break; - case "Outline": sb.Append(style.Outline); break; - case "Shadow": sb.Append(style.Shadow); break; - case "Alignment": sb.Append((int)style.Alignment); break; - case "MarginL": sb.Append(style.MarginL); break; - case "MarginR": sb.Append(style.MarginR); break; - case "MarginV": sb.Append(style.MarginV); break; - case "Encoding": sb.Append(style.Encoding); break; - default: - Trace.TraceWarning("MAPPING ERROR: Unknown field - " + field); - break; - } - if (i != v4pStyleFormat.Count - 1) - { - sb.Append(spliter); - } - } - return sb.ToString(); - } - - } // class ASSSubtitle - - public class ASSStyle - { - /// - /// The name of the Style. Case sensitive. Cannot include commas. - /// - public string Name { get; set; } - - /// - /// The fontname as used by Windows. Case-sensitive. - /// - public string Fontname { get; set; } - /// - /// The fontsize. + /// Create an ASS subtitle object, with a `Default` style and no event lines. + /// The default resolution set to the subtitle would be 1920 x 1080. /// - public double Fontsize { get; set; } - - public string PrimaryColour { get; set; } - - public string SecondaryColour { get; set; } - - public string OutlineColour { get; set; } - - public string BackColour { get; set; } - - public bool Bold { get; set; } - - public bool Italic { get; set; } - - public bool Underline { get; set; } - - public bool StrikeOut { get; set; } - - public double ScaleX { get; set; } - - public double ScaleY { get; set; } - - public int Spacing { get; set; } - - public double Angle { get; set; } - - public V4pStyleBorderStyle BorderStyle { get; set; } - - public int Outline { get; set; } - - public int Shadow { get; set; } - - public V4pStyleAlignment Alignment { get; set; } - - public int MarginL { get; set; } - - public int MarginR { get; set; } - - public int MarginV { get; set; } - - public int AlphaLevel { get; set; } - - public int Encoding { get; set; } - - public override string ToString() + /// + public static ASSSubtitle CreateNew() { - if (ASSSubtitle.showAbstract) - { - return $"{Name}: {Fontname},{Fontsize}{(Bold ? "B" : "")}{(Italic ? "I" : "")}{(Underline ? "U" : "")}{(StrikeOut ? "S" : "")}"; - } - else + var instance = new ASSSubtitle(); + instance.V4pStyleFormat = ASSStyle.DefaultFormat; + instance.EventFormat = ASSEvent.DefaultFormat; + instance.ScriptInfo = new Dictionary(); + instance.ScriptInfo.Add("PlayResX", new ASSScriptInfo { Type = ScriptInfoType.KeyValue, Key = "PlayResX", Value = "1920" }); + instance.ScriptInfo.Add("PlayResY", new ASSScriptInfo { Type = ScriptInfoType.KeyValue, Key = "PlayResY", Value = "1080" }); + instance.ScriptInfo.Add("YCbCr Matrix", new ASSScriptInfo { Type = ScriptInfoType.KeyValue, Key = "YCbCr Matrix", Value = "TV.709" }); + instance.V4pStyles = new Dictionary(); + instance.V4pStyles.Add("Default", new ASSLoader.NET.ASSStyle { - base.ToString(); - } + Name = "Default", + Fontname = "Arial", + Fontsize = 45d, + PrimaryColour = "&H00FFFFFF", + SecondaryColour = "&H0000FFFF", + OutlineColour = "&H000000FF", + BackColour = "&H000000FF", + Bold = false, + Italic = false, + Underline = false, + StrikeOut = false, + ScaleX = 100, + ScaleY = 100, + Spacing = 0, + Angle = 0, + BorderStyle = V4pStyleBorderStyle.ColorBackground, + Outline = 2, + Shadow = 2, + Alignment = V4pStyleAlignment.LeftBottom, + MarginL = 0, + MarginR = 0, + MarginV = 0, + Encoding = 1 + }); + instance.Events = new List(); + instance.UnknownSections = new Dictionary(); + return instance; } - } - - public class ASSEvent : ICloneable - { - public ASSEventType Type { get; set; } - - public int Layer { get; set; } - - public ASSEventTime Start { get; set; } - - public ASSEventTime End { get; set; } - - public string Style { get; set; } - - public string Name { get; set; } - - public int MarginL { get; set; } - - public int MarginR { get; set; } - - public int MarginV { get; set; } - - public string Effect { get; set; } - - public string Text { get; set; } - - public object Clone() - { - var newInst = new ASSEvent(); - newInst.Type = this.Type; - newInst.Layer = this.Layer; - newInst.Start = new ASSEventTime(this.Start.ToString()); - newInst.End = new ASSEventTime(this.End.ToString()); - newInst.Style = this.Style; - newInst.Name = this.Name; - newInst.MarginL = this.MarginL; - newInst.MarginR = this.MarginR; - newInst.MarginV = this.MarginV; - newInst.Effect = this.Effect; - newInst.Text = this.Text; - return newInst; - } - - public ASSEvent() - { - this.Type = ASSEventType.Dialogue; - this.Layer = 0; - this.Start = new ASSEventTime(0, 0, 0, 0); - this.End = new ASSEventTime(0, 0, 0, 0); - this.Style = "Default"; - this.Name = string.Empty; - this.MarginL = 0; - this.MarginR = 0; - this.MarginV = 0; - this.Effect = string.Empty; - this.Text = string.Empty; - } - - public override string ToString() - { - if (ASSSubtitle.showAbstract) - { - return $"{Start} - {End} | {Type}:{Name}:{Text}"; - } - else - { - base.ToString(); - } - } - } - - public class ASSEventTime : IComparable - { - public int Hour { get; set; } - - public int Minute { get; set; } - - public int Second { get; set; } - - public int Millisecond { get; set; } - - public ASSEventTime(string assTime) - { - var parts = assTime.Split(':', '.'); - var msIndex = parts.Length - 1; - var secIndex = parts.Length - 2; - var minIndex = parts.Length - 3; - var hourIndex = parts.Length - 4; - this.Hour = hourIndex > 0 ? Convert.ToInt32(parts[hourIndex]) : 0; - this.Minute = minIndex > 0 ? Convert.ToInt32(parts[minIndex]) : 0; - this.Second = secIndex > 0 ? Convert.ToInt32(parts[secIndex]) : 0; - this.Millisecond = msIndex > 0 ? Convert.ToInt32((parts[msIndex] + "000").Substring(0, 3)) : 0; - } - - public ASSEventTime(int hour, int minute, int second, int millisecond) - { - this.Hour = hour; - this.Minute = minute; - this.Second = second; - this.Millisecond = millisecond; - } - - public long TotalMilliseconds() - { - return this.Hour * 3600000 + this.Minute * 60000 + this.Second * 1000 + this.Millisecond; - } - - public static explicit operator ASSEventTime(TimeSpan ts) - { - return new ASSEventTime(ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds); - } - - public static explicit operator TimeSpan(ASSEventTime time) - { - return new TimeSpan(0, time.Hour, time.Minute, time.Second, time.Millisecond); - } - - public static ASSEventTime operator +(ASSEventTime aet, double num) - { - var target = new ASSEventTime(aet.ToString()); - var ms = Convert.ToInt32(Math.Floor(num * 1000)); - target.Millisecond = target.Millisecond + ms; - if (target.Millisecond > 1000) - { - target.Second += target.Millisecond / 1000; - target.Millisecond = target.Millisecond % 1000; - } - if (target.Second > 60) - { - target.Minute += target.Second / 60; - target.Second = target.Second % 60; - } - if(target.Minute > 60) - { - target.Hour += target.Minute / 60; - target.Minute = target.Minute % 60; - } - - return target; - } - - public static ASSEventTime operator -(ASSEventTime aet, double num) - { - var ms = Convert.ToInt32(Math.Floor(num * 1000)); - var target = new ASSEventTime(aet.ToString()); - target.Millisecond = aet.Millisecond - ms; - if (target.Millisecond < 0) - { - target.Millisecond += 1000; - target.Second -= 1; - } - if (target.Second < 0) - { - target.Second += 60; - target.Minute -= 1; - } - if (target.Minute < 0) - { - target.Minute += 60; - target.Hour -= 1; - } - return target; - } - - public override bool Equals(object obj) - { - return CompareTo(obj) == 0; - } - - public override int GetHashCode() - { - return (int)this.TotalMilliseconds(); - } - public override string ToString() - { - return this.Hour.ToString().Substring(0, 1) + ":" - + this.Minute.ToString().PadLeft(2, '0') + ":" - + this.Second.ToString().PadLeft(2, '0') + "." - + this.Millisecond.ToString().PadLeft(3, '0').Substring(0, 2); - } - - public int CompareTo(object obj) - { - var target = obj as ASSEventTime; - if (target == null) - { - target = new ASSEventTime(obj.ToString()); - } - return (int)(this.TotalMilliseconds() - target.TotalMilliseconds()); - } - } - - public class ASSEmbeddedFont - { - - } - - public class ASSEmbeddedGraphics - { - - } - - public enum V4pStyleBorderStyle - { - BorderAndShadow = 1, - ColorBackground = 3 - } - - public enum V4pStyleAlignment - { - SubLF = 1, - SubCT = 2, - SubRT = 3, - MidLF = 4, - MidCT = 5, - MidRT = 6, - TopLF = 7, - TopCT = 8, - TopRT = 9 - } - - public enum ASSEventType - { - Dialogue, - Comment, - Picture, - Sound, - Movie, - Command - } + } // class ASSSubtitle - public enum ASSSection - { - Unknown, - ScriptInfo, - V4pStyles, - Events, - Fonts, - Graphics - } } diff --git a/ASSLoader.NET/Enums.cs b/ASSLoader.NET/Enums.cs new file mode 100644 index 0000000..1371138 --- /dev/null +++ b/ASSLoader.NET/Enums.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ASSLoader.NET.Enums +{ + public class ASSEmbeddedFont + { + + } + + public class ASSEmbeddedGraphics + { + + } + + public enum ScriptInfoType + { + Unspecified, + Comments, + KeyValue + } + + public enum V4pStyleBorderStyle + { + BorderAndShadow = 1, + ColorBackground = 3 + } + + public enum V4pStyleAlignment + { + LeftBottom = 1, + CenterBottom = 2, + RightBottom = 3, + LeftMiddle = 4, + CenterMiddle = 5, + RightMiddle = 6, + LeftTop = 7, + CenterTop = 8, + RightTop = 9 + } + + public enum ASSEventType + { + Dialogue, + Comment, + Picture, + Sound, + Movie, + Command + } + + public enum ASSSection + { + Unknown, + ScriptInfo, + V4pStyles, + Events, + Fonts, + Graphics + } +} diff --git a/README.assets/11740438 b/README.assets/11740438 new file mode 100644 index 0000000..10edd49 Binary files /dev/null and b/README.assets/11740438 differ diff --git a/README.md b/README.md index 7207062..a1ba77a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ # ASSLoader.NET A .NET Library for ass subtitle file loading and writing. + +# Update Logs + +## v0.9.2 - 2021-05-14 + +* Divide the `ASSSubtitle.cs` into files by classes. +* Add some document comments. +* Add `DefaultFormat` for `ASSEvent` and `ASSStyle`. +* Add `CreateNew()` for `ASSSubtitle` to create new ass file with a default template with `Default` style and empty events. +* Fixed bugs by @no1d + +TODO: + +* Complete document comments. +* Full reference document. + +v0.9.1 - 2020-05-14 +--------------------- +* When load ass file, unknown sections would be also stored to object. + So the unknown sections would be write back to the file. +* Make StartTime and EndTime of each subtitle line to a managed type + - `ASSEventTime` + +# Contributor + +  +