├── AssParser.Test ├── GlobalUsings.cs ├── UUEncodeTest │ ├── FreeSans.ttf │ └── UUEncodeTest.cs ├── xunit.runner.json ├── AssParserExtTest │ ├── FontsTest.txt │ ├── AssParserExtTest.cs │ └── FontsTest.ass ├── AssParserTest │ ├── event_23.ass │ ├── format_14.ass │ ├── format_19.ass │ ├── style_15.ass │ └── AssParserTest.cs └── AssParser.Test.csproj ├── AssParser.Lib ├── AssParser.Lib.csproj ├── AssParserException.cs ├── UUEncode.cs ├── AssParserExt.cs ├── AssSubtitleModel.cs └── AssParser.cs ├── AssParser ├── AssParser.csproj ├── ParserBenchmark.cs └── Program.cs ├── LICENSE ├── .github └── workflows │ ├── test.yml │ └── dotnet-nuget.yml ├── AssParser.sln ├── README.md ├── .gitattributes └── .gitignore /AssParser.Test/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /AssParser.Test/UUEncodeTest/FreeSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmusementClub/AssParser/master/AssParser.Test/UUEncodeTest/FreeSans.ttf -------------------------------------------------------------------------------- /AssParser.Test/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "maxParallelThreads": -1, 4 | "parallelizeAssembly": false, 5 | "parallelizeTestCollections": false, 6 | "stopOnFail": true 7 | } -------------------------------------------------------------------------------- /AssParser.Test/AssParserExtTest/FontsTest.txt: -------------------------------------------------------------------------------- 1 | Noto Sans 700 False 丁 2 | Noto Sans 0 True そさしすせ 3 | Noto Sans 300 False 丙 4 | TH Baijam 1 False xyz 5 | Noto Sans 200 False 乙 6 | FOT-TsukuARdGothic Std R 0 False   7 | Noto Sans 1 True まみむめも 8 | TH Baijam 0 False てhiとjuたvwopfqg 9 | Noto Sans 100 False 甲 10 | Noto Sans 1 False tこきbcくderけかsa 11 | Noto Sans 0 False nえあkおいlmう 12 | -------------------------------------------------------------------------------- /AssParser.Lib/AssParser.Lib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0;net6.0 5 | enable 6 | enable 7 | README.md 8 | MIT 9 | 1.2.1 10 | https://github.com/AmusementClub/AssParser.git 11 | git 12 | 13 | 14 | 15 | 16 | True 17 | \ 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /AssParser/AssParser.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /AssParser/ParserBenchmark.cs: -------------------------------------------------------------------------------- 1 | using AssParser.Lib; 2 | using BenchmarkDotNet.Attributes; 3 | using BenchmarkDotNet.Jobs; 4 | 5 | namespace AssParser 6 | { 7 | [SimpleJob(RuntimeMoniker.Net70, baseline: true)] 8 | [SimpleJob(RuntimeMoniker.NativeAot70)] 9 | [RPlotExporter] 10 | public class ParserBenchmark 11 | { 12 | public AssSubtitleModel assfile; 13 | [GlobalSetup] 14 | public void setup() 15 | { 16 | assfile= Lib.AssParser.ParseAssFile(@"[Nekomoe kissaten&VCB-Studio] Cider no You ni Kotoba ga Wakiagaru [Ma10p_1080p][x265_flac].jp&sc.ass").Result; 17 | } 18 | [Benchmark] 19 | public void ParserBenchmarkTest() 20 | { 21 | Lib.AssParser.ParseAssFile(@"[Nekomoe kissaten&VCB-Studio] Cider no You ni Kotoba ga Wakiagaru [Ma10p_1080p][x265_flac].jp&sc.ass").Wait(); 22 | } 23 | [Benchmark] 24 | public void ParserBenchmarkTest2() 25 | { 26 | var fonts = assfile.UsedFonts(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 私立七森中ごらく部 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: All test 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | build-test: 12 | name: Build & Test 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Setup .NET 18 | uses: actions/setup-dotnet@v3 19 | with: 20 | dotnet-version: 7.0.x 21 | - name: Restore dependencies 22 | run: dotnet restore 23 | - name: Build 24 | run: dotnet build --no-restore 25 | - name: Test 26 | run: dotnet test --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" 27 | - name: Test Reporter 28 | # You may pin to the exact commit or the version. 29 | # uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 30 | uses: dorny/test-reporter@v1.6.0 31 | if: success() || failure() # run this step even if previous step failed 32 | with: 33 | name: xUnit Tests # Name of the check run which will be created 34 | path: '**/test-results.trx' # Path to test results 35 | reporter: dotnet-trx # Format of test results 36 | 37 | -------------------------------------------------------------------------------- /AssParser.Test/AssParserExtTest/AssParserExtTest.cs: -------------------------------------------------------------------------------- 1 | using AssParser.Lib; 2 | 3 | namespace AssParser.Test.AssParserExtTest 4 | { 5 | public class AssParserExtTest 6 | { 7 | [Fact] 8 | public void UsedFonts_ShouldBe_Equivalent() 9 | { 10 | var truth = File.ReadAllLines(Path.Combine("AssParserExtTest", "FontsTest.txt")); 11 | var sortedTruth = new List(); 12 | foreach (var line in truth) 13 | { 14 | var parts = line.Split('\t'); 15 | parts[3] = SortString(parts[3]); 16 | sortedTruth.Add(string.Join("\t", parts)); 17 | } 18 | var assfile = Lib.AssParser.ParseAssFile(Path.Combine("AssParserExtTest", "FontsTest.ass")).Result; 19 | var fonts = assfile.UsedFonts(); 20 | var res = new List(); 21 | foreach (var font in fonts) 22 | { 23 | res.Add(font.FontName + "\t" + font.Bold + "\t" + font.IsItalic + "\t" + SortString(font.UsedChar)); 24 | } 25 | Assert.Equivalent(sortedTruth.ToHashSet(), res.ToHashSet()); 26 | } 27 | private static string SortString(string s) 28 | { 29 | var sa = s.ToArray(); 30 | Array.Sort(sa); 31 | return new(sa); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-nuget.yml: -------------------------------------------------------------------------------- 1 | name: Publish to nuget 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | configuration: [ Release ] 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Set env 19 | run: echo "RELEASE_VERSION=$(echo ${GITHUB_REF#refs/*/} | grep -Po "(?<=^v).*")" >> $GITHUB_ENV 20 | - name: Test 21 | run: | 22 | echo $RELEASE_VERSION 23 | echo ${{ env.RELEASE_VERSION }} 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | - name: Setup .NET Core SDK 28 | uses: actions/setup-dotnet@v3 29 | with: 30 | dotnet-version: | 31 | 6.x 32 | 7.x 33 | 34 | - name: Publish the application 35 | run: dotnet pack AssParser.Lib --configuration ${{ matrix.configuration }} -p:PackageVersion=$RELEASE_VERSION 36 | 37 | - name: Upload build artifacts 38 | uses: actions/upload-artifact@v3 39 | with: 40 | name: AssParser.Lib.$RELEASE_VERSION.nupkg 41 | path: | 42 | AssParser.Lib/bin/${{ matrix.configuration }}/* 43 | 44 | - name: Publish to Nuget 45 | run: dotnet nuget push AssParser.Lib/bin/${{ matrix.configuration }}/AssParser.Lib.$RELEASE_VERSION.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json 46 | -------------------------------------------------------------------------------- /AssParser/Program.cs: -------------------------------------------------------------------------------- 1 | using AssParser.Lib; 2 | 3 | namespace AssParser 4 | { 5 | internal class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | try 10 | { 11 | var assfile = Lib.AssParser.ParseAssFile(@"[Nekomoe kissaten&VCB-Studio] Cider no You ni Kotoba ga Wakiagaru [Ma10p_1080p][x265_flac].jp&sc.ass").Result; 12 | var fonts = assfile.UsedFonts(); 13 | foreach (var font in fonts) 14 | { 15 | Console.WriteLine(font.FontName + "\t" + font.UsedChar); 16 | } 17 | var txt = assfile.ToString(); 18 | } 19 | catch (AggregateException ae) 20 | { 21 | foreach (var ex in ae.InnerExceptions) 22 | { 23 | // Handle the custom exception. 24 | if (ex is AssParserException) 25 | { 26 | var assExcption = ex as AssParserException; 27 | Console.WriteLine(assExcption?.ToString()); 28 | } 29 | // Rethrow any other exception. 30 | else 31 | { 32 | Console.WriteLine(ex.Message); 33 | } 34 | } 35 | } 36 | #if !DEBUG 37 | var summary = BenchmarkRunner.Run(); 38 | #endif 39 | Console.ReadLine(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /AssParser.Test/AssParserTest/event_23.ass: -------------------------------------------------------------------------------- 1 | [Script Info] 2 | ; Script generated by Aegisub 0-mod-d5c132a 3 | ; http://www.aegisub.org/ 4 | Comment: [Processed by 繁化姬 dict-7ac3466c-r881 @ 2019/08/22 15:37:24 | https://zhconvert.org 5 | Title: [Nekomoe kissaten] Uma Musume [14][BDRip].JPSC 6 | ScriptType: v4.00+ 7 | WrapStyle: 2 8 | ScaledBorderAndShadow: yes 9 | PlayResX: 1280 10 | PlayResY: 720 11 | YCbCr Matrix: TV.709 12 | 13 | [V4+ Styles] 14 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 15 | Style: Dial-CH,Tensentype JiaLiDaYuanGB18030,38,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,2,10,10,30,1 16 | Style: Dial-CH2,Tensentype JiaLiDaYuanGB18030,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,8,10,10,36,1 17 | 18 | [Events] 19 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 20 | Comment: 0,0:00:00.00,0:00:00.00,Staff,,0,0,0,,Staff 21 | Dialogue: 0,0:00:05.00,0:00:13.41,Staff,,0,0,0,,{=0}{\fscy100\t(5,8388,\fscy148.63)\fscx100\t(5,8388,\fscx148.63)\move(640,10,672.24,-73.86,5,8388)\alpha&HFF&\t(0,500,\alpha&H00&)\t(7910,8410,\alpha&HFF&)\blur5}本字幕由喵萌奶茶屋制作 仅供交流试看之用 请勿用于商业用途 \N翻译: Ronny 校对: 雨后飘雪 繁化: SashiharaRino 后期: MIR 22 | Comment: 0,0:00:00.00,0:00:00.00,Staff,,0,0,0,,Title 23 | Dialogu: 0,0:02:03.98,0:02:07.98,Title,,0,0,0,,{\clip(452,502,834,504)\pos(640,570)\c&H99A8F2&}BNW的誓言① 24 | Dialogue: 0,0:02:03.98,0:02:07.98,Title,,0,0,0,,{\clip(452,504,834,506)\pos(640,570)\c&H99A8F2&}BNW的誓言① 25 | -------------------------------------------------------------------------------- /AssParser.Test/AssParserTest/format_14.ass: -------------------------------------------------------------------------------- 1 | [Script Info] 2 | ; Script generated by Aegisub 0-mod-d5c132a 3 | ; http://www.aegisub.org/ 4 | Comment: [Processed by 繁化姬 dict-7ac3466c-r881 @ 2019/08/22 15:37:24 | https://zhconvert.org 5 | Title: [Nekomoe kissaten] Uma Musume [14][BDRip].JPSC 6 | ScriptType: v4.00+ 7 | WrapStyle: 2 8 | ScaledBorderAndShadow: yes 9 | PlayResX: 1280 10 | PlayResY: 720 11 | YCbCr Matrix: TV.709 12 | 13 | [V4+ Styles] 14 | Format: Names, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 15 | Style: Dial-CH,Tensentype JiaLiDaYuanGB18030,38,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,2,10,10,30,1 16 | Style: Dial-CH2,Tensentype JiaLiDaYuanGB18030,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,8,10,10,36,1 17 | 18 | [Events] 19 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 20 | Comment: 0,0:00:00.00,0:00:00.00,Staff,,0,0,0,,Staff 21 | Dialogue: 0,0:00:05.00,0:00:13.41,Staff,,0,0,0,,{=0}{\fscy100\t(5,8388,\fscy148.63)\fscx100\t(5,8388,\fscx148.63)\move(640,10,672.24,-73.86,5,8388)\alpha&HFF&\t(0,500,\alpha&H00&)\t(7910,8410,\alpha&HFF&)\blur5}本字幕由喵萌奶茶屋制作 仅供交流试看之用 请勿用于商业用途 \N翻译: Ronny 校对: 雨后飘雪 繁化: SashiharaRino 后期: MIR 22 | Comment: 0,0:00:00.00,0:00:00.00,Staff,,0,0,0,,Title 23 | Dialogue: 0,0:02:03.98,0:02:07.98,Title,,0,0,0,,{\clip(452,502,834,504)\pos(640,570)\c&H99A8F2&}BNW的誓言① 24 | Dialogue: 0,0:02:03.98,0:02:07.98,Title,,0,0,0,,{\clip(452,504,834,506)\pos(640,570)\c&H99A8F2&}BNW的誓言① 25 | -------------------------------------------------------------------------------- /AssParser.Test/AssParserTest/format_19.ass: -------------------------------------------------------------------------------- 1 | [Script Info] 2 | ; Script generated by Aegisub 0-mod-d5c132a 3 | ; http://www.aegisub.org/ 4 | Comment: [Processed by 繁化姬 dict-7ac3466c-r881 @ 2019/08/22 15:37:24 | https://zhconvert.org 5 | Title: [Nekomoe kissaten] Uma Musume [14][BDRip].JPSC 6 | ScriptType: v4.00+ 7 | WrapStyle: 2 8 | ScaledBorderAndShadow: yes 9 | PlayResX: 1280 10 | PlayResY: 720 11 | YCbCr Matrix: TV.709 12 | 13 | [V4+ Styles] 14 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 15 | Style: Dial-CH,Tensentype JiaLiDaYuanGB18030,38,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,2,10,10,30,1 16 | Style: Dial-CH2,Tensentype JiaLiDaYuanGB18030,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,8,10,10,36,1 17 | 18 | [Events] 19 | Format: Laye, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 20 | Comment: 0,0:00:00.00,0:00:00.00,Staff,,0,0,0,,Staff 21 | Dialogue: 0,0:00:05.00,0:00:13.41,Staff,,0,0,0,,{=0}{\fscy100\t(5,8388,\fscy148.63)\fscx100\t(5,8388,\fscx148.63)\move(640,10,672.24,-73.86,5,8388)\alpha&HFF&\t(0,500,\alpha&H00&)\t(7910,8410,\alpha&HFF&)\blur5}本字幕由喵萌奶茶屋制作 仅供交流试看之用 请勿用于商业用途 \N翻译: Ronny 校对: 雨后飘雪 繁化: SashiharaRino 后期: MIR 22 | Comment: 0,0:00:00.00,0:00:00.00,Staff,,0,0,0,,Title 23 | Dialogue: 0,0:02:03.98,0:02:07.98,Title,,0,0,0,,{\clip(452,502,834,504)\pos(640,570)\c&H99A8F2&}BNW的誓言① 24 | Dialogue: 0,0:02:03.98,0:02:07.98,Title,,0,0,0,,{\clip(452,504,834,506)\pos(640,570)\c&H99A8F2&}BNW的誓言① 25 | -------------------------------------------------------------------------------- /AssParser.Test/AssParserTest/style_15.ass: -------------------------------------------------------------------------------- 1 | [Script Info] 2 | ; Script generated by Aegisub 0-mod-d5c132a 3 | ; http://www.aegisub.org/ 4 | Comment: [Processed by 繁化姬 dict-7ac3466c-r881 @ 2019/08/22 15:37:24 | https://zhconvert.org 5 | Title: [Nekomoe kissaten] Uma Musume [14][BDRip].JPSC 6 | ScriptType: v4.00+ 7 | WrapStyle: 2 8 | ScaledBorderAndShadow: yes 9 | PlayResX: 1280 10 | PlayResY: 720 11 | YCbCr Matrix: TV.709 12 | 13 | [V4+ Styles] 14 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 15 | Styl: Dial-CH,Tensentype JiaLiDaYuanGB18030,38,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,2,10,10,30,1 16 | Style: Dial-CH2,Tensentype JiaLiDaYuanGB18030,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,8,10,10,36,1 17 | 18 | [Events] 19 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 20 | Comment: 0,0:00:00.00,0:00:00.00,Staff,,0,0,0,,Staff 21 | Dialogue: 0,0:00:05.00,0:00:13.41,Staff,,0,0,0,,{=0}{\fscy100\t(5,8388,\fscy148.63)\fscx100\t(5,8388,\fscx148.63)\move(640,10,672.24,-73.86,5,8388)\alpha&HFF&\t(0,500,\alpha&H00&)\t(7910,8410,\alpha&HFF&)\blur5}本字幕由喵萌奶茶屋制作 仅供交流试看之用 请勿用于商业用途 \N翻译: Ronny 校对: 雨后飘雪 繁化: SashiharaRino 后期: MIR 22 | Comment: 0,0:00:00.00,0:00:00.00,Staff,,0,0,0,,Title 23 | Dialogue: 0,0:02:03.98,0:02:07.98,Title,,0,0,0,,{\clip(452,502,834,504)\pos(640,570)\c&H99A8F2&}BNW的誓言① 24 | Dialogue: 0,0:02:03.98,0:02:07.98,Title,,0,0,0,,{\clip(452,504,834,506)\pos(640,570)\c&H99A8F2&}BNW的誓言① 25 | -------------------------------------------------------------------------------- /AssParser.Test/AssParserTest/AssParserTest.cs: -------------------------------------------------------------------------------- 1 | using AssParser.Lib; 2 | 3 | namespace AssParser.Test.AssParserTest 4 | { 5 | public class AssParserTest 6 | { 7 | [Theory] 8 | [InlineData("1.ass")] 9 | [InlineData("2.ass")] 10 | public void AssParser_ShouldNot_Throw(string file) 11 | { 12 | //Arrange 13 | var path = Path.Combine("AssParserTest", file); 14 | 15 | //Act 16 | var exception = Record.ExceptionAsync(() => Lib.AssParser.ParseAssFile(path)); 17 | 18 | //Assert 19 | Assert.Null(exception.Result); 20 | } 21 | [Theory] 22 | [InlineData("1.ass")] 23 | [InlineData("2.ass")] 24 | public void ToString_ShouldBe_Same(string file) 25 | { 26 | //Arrange 27 | var path = Path.Combine("AssParserTest", file); 28 | var source = File.ReadAllText(path); 29 | var assfile = Lib.AssParser.ParseAssFile(path).Result; 30 | 31 | //Act 32 | var res = assfile.ToString(); 33 | 34 | //Assert 35 | Assert.Equal(source.Replace("\r\n", "\n").Replace("\n", "\r\n"), res.Replace("\r\n", "\n").Replace("\n", "\r\n")); 36 | } 37 | [Fact] 38 | public async void AssParser_ShouldThrow_InvalidStyle() 39 | { 40 | //Arrange 41 | var path = Path.Combine("AssParserTest", "format_14.ass"); 42 | using var sr = new StreamReader(File.OpenRead(path)); 43 | 44 | //Act 45 | var act = () => Lib.AssParser.ParseAssFile(sr); 46 | 47 | //Assert 48 | var exception = await Assert.ThrowsAsync(act); 49 | Assert.Equal(14, exception?.LineCount); 50 | Assert.Equal(AssParserErrorType.InvalidStyleLine, exception?.ErrorType); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /AssParser.Test/UUEncodeTest/UUEncodeTest.cs: -------------------------------------------------------------------------------- 1 | using AssParser.Lib; 2 | using System.Text.RegularExpressions; 3 | using Xunit.Abstractions; 4 | 5 | namespace AssParser.Test.UUEncodeTest 6 | { 7 | public class UUEncodeTest 8 | { 9 | private readonly ITestOutputHelper _testOutputHelper; 10 | private readonly string fontsDataCrlf; 11 | private readonly string fontsDataLf; 12 | 13 | public UUEncodeTest(ITestOutputHelper testOutputHelper) 14 | { 15 | _testOutputHelper = testOutputHelper; 16 | var assfile = Lib.AssParser.ParseAssFile(Path.Combine("UUEncodeTest", "1.ass")).Result; 17 | var fontsData = assfile.UnknownSections["[Fonts]"]; 18 | fontsData = fontsData.Remove(0, fontsData.IndexOf("\n", StringComparison.Ordinal) + 1).Trim(); 19 | 20 | fontsDataCrlf = Regex.Replace(fontsData, "\r?\n", "\r\n"); 21 | fontsDataLf = Regex.Replace(fontsData, "\r?\n", "\n"); 22 | } 23 | 24 | [Fact] 25 | public void UUDecode_ShouldBe_Same() 26 | { 27 | var ttf = File.ReadAllBytes(Path.Combine("UUEncodeTest", "FreeSans.ttf")); 28 | var data1 = UUEncode.Decode(fontsDataCrlf, out _); 29 | Assert.Equal(ttf, data1); 30 | } 31 | 32 | [Fact] 33 | public void UUEncode_ShouldBe_Same_Crlf() 34 | { 35 | var data1 = UUEncode.Decode(fontsDataCrlf, out var crlf); 36 | var encoded = UUEncode.Encode(data1, true, crlf); 37 | Assert.Equal(fontsDataCrlf, encoded); 38 | } 39 | 40 | [Fact] 41 | public void UUEncode_ShouldBe_Same_Lf() 42 | { 43 | var data1 = UUEncode.Decode(fontsDataLf, out var crlf); 44 | var encoded = UUEncode.Encode(data1, true, crlf); 45 | Assert.Equal(fontsDataLf, encoded); 46 | } 47 | 48 | [Fact] 49 | public void UUEncode_Encode_Coverage_Test() 50 | { 51 | Assert.Equal("-1", UUEncode.Encode("1"u8.ToArray())); 52 | Assert.Equal("-4%", UUEncode.Encode("11"u8.ToArray())); 53 | Assert.Equal("-4%R", UUEncode.Encode("111"u8.ToArray())); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /AssParser.Test/AssParserExtTest/FontsTest.ass: -------------------------------------------------------------------------------- 1 | [Script Info] 2 | Title: Fonts Test 3 | ScriptType: v4.00+ 4 | WrapStyle: 0 5 | ScaledBorderAndShadow: yes 6 | PlayResX: 1920 7 | PlayResY: 1080 8 | YCbCr Matrix: TV.709 9 | 10 | [V4+ Styles] 11 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 12 | Style: Default,Noto Sans,100,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,5,1.5,2,96,96,65,1 13 | Style: Bold,Noto Sans,50,&H00F4F6F6,&H000019FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,0,0,5,10,10,10,1 14 | Style: Italic,Noto Sans,100,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,-1,0,0,100,100,0,0,1,5,1.5,8,96,96,33,1 15 | Style: Bold_Italic,Noto Sans,90,&H00FFFFFF,&H00FFFFFF,&H00001E06,&H00000000,-1,-1,0,0,100,100,0,0,1,5,1.5,7,96,96,65,1 16 | Style: Other,TH Baijam,133,&H00EEFBDB,&H000019FF,&H0001031B,&H00000000,0,0,0,0,100,100,0,0,1,13,2,2,10,10,10,1 17 | Style: Space,FOT-TsukuARdGothic Std R,133,&H00EEFBDB,&H000019FF,&H0001031B,&H00000000,0,0,0,0,100,100,0,0,1,13,2,2,10,10,10,1 18 | 19 | 20 | [Events] 21 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 22 | Dialogue: 0,0:02:07.06,0:02:10.10,Default,,0,0,0,,あいうえお 23 | Dialogue: 0,0:02:07.06,0:02:10.10,Bold,,0,0,0,,かきくけこ 24 | Dialogue: 0,0:02:09.94,0:02:12.02,Italic,,0,0,0,,さしすせそ 25 | Dialogue: 0,0:02:07.06,0:02:10.10,Bold_Italic,,0,0,0,,まみむめも 26 | Dialogue: 0,0:02:09.94,0:02:12.02,Other,,0,0,0,,たてと 27 | Dialogue: 0,0:03:21.98,0:03:23.64,Default,,0,0,0,,{\rBold}abcde 28 | Dialogue: 0,0:03:21.98,0:03:23.64,Default,,0,0,0,,{\rOther}fghij 29 | Dialogue: 0,0:03:23.64,0:03:27.14,Default,,0,0,0,,{\b1\i1\r}klmn 30 | Dialogue: 0,0:03:23.64,0:03:27.14,Default,,0,0,0,,{\fnTH Baijam}opq 31 | Dialogue: 0,0:04:12.70,0:04:15.31,Default,,0,0,0,,{\b1\i1\rBold}rst{\rSpace}\n\N{\rOther}uvw{\b1}xyz 32 | Dialogue: 0,0:04:12.70,0:04:15.31,Space,,0,0,0,,\n\N\h 33 | Dialogue: 0,0:04:34.83,0:04:39.00,Default,,0,0,0,,{\b100}甲 34 | Dialogue: 0,0:04:34.83,0:04:39.00,Default,,0,0,0,,{\b200}乙 35 | Dialogue: 0,0:04:34.83,0:04:39.00,Default,,0,0,0,,{\b300}丙 36 | Dialogue: 0,0:04:34.83,0:04:39.00,Default,,0,0,0,,{\b700}丁 -------------------------------------------------------------------------------- /AssParser.Test/AssParser.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | PreserveNewest 32 | 33 | 34 | PreserveNewest 35 | 36 | 37 | PreserveNewest 38 | 39 | 40 | PreserveNewest 41 | 42 | 43 | PreserveNewest 44 | 45 | 46 | PreserveNewest 47 | 48 | 49 | PreserveNewest 50 | 51 | 52 | PreserveNewest 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /AssParser.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33424.131 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssParser", "AssParser\AssParser.csproj", "{D35EC867-62B6-43EC-BF1B-7F3EC4266614}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssParser.Lib", "AssParser.Lib\AssParser.Lib.csproj", "{54A77E0E-F7CC-4185-B264-8EB59A9D6265}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssParser.Test", "AssParser.Test\AssParser.Test.csproj", "{315F3264-222E-49C7-A259-7CA1417EFB36}" 11 | ProjectSection(ProjectDependencies) = postProject 12 | {54A77E0E-F7CC-4185-B264-8EB59A9D6265} = {54A77E0E-F7CC-4185-B264-8EB59A9D6265} 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {D35EC867-62B6-43EC-BF1B-7F3EC4266614}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {D35EC867-62B6-43EC-BF1B-7F3EC4266614}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {D35EC867-62B6-43EC-BF1B-7F3EC4266614}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {D35EC867-62B6-43EC-BF1B-7F3EC4266614}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {54A77E0E-F7CC-4185-B264-8EB59A9D6265}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {54A77E0E-F7CC-4185-B264-8EB59A9D6265}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {54A77E0E-F7CC-4185-B264-8EB59A9D6265}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {54A77E0E-F7CC-4185-B264-8EB59A9D6265}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {315F3264-222E-49C7-A259-7CA1417EFB36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {315F3264-222E-49C7-A259-7CA1417EFB36}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {315F3264-222E-49C7-A259-7CA1417EFB36}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {315F3264-222E-49C7-A259-7CA1417EFB36}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(SolutionProperties) = preSolution 35 | HideSolutionNode = FALSE 36 | EndGlobalSection 37 | GlobalSection(ExtensibilityGlobals) = postSolution 38 | SolutionGuid = {1122EA1F-81AD-4EA4-B653-43C45210BF5E} 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AssParser · [![Publish to nuget](https://github.com/AmusementClub/AssParser/actions/workflows/dotnet-nuget.yml/badge.svg)](https://github.com/AmusementClub/AssParser/actions/workflows/dotnet-nuget.yml) ![Nuget](https://img.shields.io/nuget/v/AssParser.Lib?logo=nuget) [![Test](https://github.com/AmusementClub/AssParser/actions/workflows/test.yml/badge.svg)](https://github.com/AmusementClub/AssParser/actions/workflows/test.yml) 2 | 3 | Parse ASS(SubStation Alpha Subtitles) file faster. No Regex. All managed code. 4 | 5 | ## Basic Parse 6 | 7 | ``` cs 8 | AssSubtitleModel assfile = Lib.AssParser.ParseAssFile(@"path/to/your/assfile").Result; 9 | 10 | # Or async way 11 | 12 | AssSubtitleModel assfile = await Lib.AssParser.ParseAssFile(@"path/to/your/assfile"); 13 | ``` 14 | 15 | ## List used fonted 16 | ``` cs 17 | AssSubtitleModel assfile = Lib.AssParser.ParseAssFile(@"path/to/your/assfile").Result; 18 | FontDetail[] fonts = assfile.UsedFonts(); 19 | ``` 20 | Where FontDetail is defined as 21 | ``` cs 22 | public class FontDetail : IEquatable 23 | { 24 | public string FontName = ""; 25 | public string UsedChar = ""; 26 | public int Bold; 27 | public bool IsItalic; 28 | 29 | public override bool Equals(object? obj) 30 | { 31 | return Equals(obj as FontDetail); 32 | } 33 | 34 | public bool Equals(FontDetail? other) 35 | { 36 | return other is not null && 37 | FontName == other.FontName && 38 | Bold == other.Bold && 39 | IsItalic == other.IsItalic; 40 | } 41 | 42 | public override int GetHashCode() 43 | { 44 | return HashCode.Combine(FontName, Bold, IsItalic); 45 | } 46 | 47 | public static bool operator ==(FontDetail? left, FontDetail? right) 48 | { 49 | return EqualityComparer.Default.Equals(left, right); 50 | } 51 | 52 | public static bool operator !=(FontDetail? left, FontDetail? right) 53 | { 54 | return !(left == right); 55 | } 56 | } 57 | ``` 58 | 59 | ## Get extra section 60 | ``` cs 61 | AssSubtitleModel assfile = Lib.AssParser.ParseAssFile(Path.Combine("UUEncodeTest", "1.ass")).Result; 62 | string fontsData = assfile.UnknownSections["[Fonts]"]; 63 | ``` 64 | 65 | ## Decode & Encode UUEncode 66 | ``` cs 67 | byte[] data = UUEncode.Decode(fontsData, out var crlf); 68 | string encoded = UUEncode.Eecode(data, crlf) 69 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /AssParser.Lib/AssParserException.cs: -------------------------------------------------------------------------------- 1 | namespace AssParser.Lib 2 | { 3 | [Serializable] 4 | public class AssParserException : Exception 5 | { 6 | public StreamReader streamReader; 7 | public int LineCount; 8 | public AssParserErrorType ErrorType; 9 | public AssParserException(StreamReader streamReader, int lineCount, AssParserErrorType errorType) 10 | { 11 | this.streamReader = streamReader; 12 | LineCount = lineCount; 13 | ErrorType = errorType; 14 | } 15 | public AssParserException(string message, StreamReader streamReader, int lineCount, AssParserErrorType errorType) : base(message) 16 | { 17 | streamReader.DiscardBufferedData(); 18 | this.streamReader = streamReader; 19 | LineCount = lineCount; 20 | ErrorType = errorType; 21 | } 22 | public AssParserException(string message, Exception inner, StreamReader streamReader, int lineCount, AssParserErrorType errorType) : base(message, inner) 23 | { 24 | this.streamReader = streamReader; 25 | LineCount = lineCount; 26 | ErrorType = errorType; 27 | } 28 | protected AssParserException( 29 | System.Runtime.Serialization.SerializationInfo info, 30 | System.Runtime.Serialization.StreamingContext context, StreamReader streamReader, int lineCount, AssParserErrorType errorType) : base(info, context) 31 | { 32 | this.streamReader = streamReader; 33 | LineCount = lineCount; 34 | ErrorType = errorType; 35 | } 36 | /// 37 | /// Print readable exception in English. 38 | /// 39 | /// Exception message and line content. 40 | public override string ToString() 41 | { 42 | string? Line = PrintErrorLine(); 43 | return $"{base.Message}{Environment.NewLine}{Line}"; 44 | } 45 | /// 46 | /// Print the line where exception occurs. 47 | /// 48 | /// Line number and the content of the line.The format is "Line XX : ********". 49 | public string? PrintErrorLine() 50 | { 51 | streamReader.DiscardBufferedData(); 52 | streamReader.BaseStream.Position = 0; 53 | int i = 1; 54 | while (i < LineCount) 55 | { 56 | i++; 57 | _ = streamReader.ReadLine(); 58 | } 59 | return $"Line {LineCount} : {streamReader.ReadLine()}"; 60 | } 61 | } 62 | public enum AssParserErrorType 63 | { 64 | UnknownError, 65 | InvalidSection, 66 | MissingFormatLine, 67 | InvalidStyleLine, 68 | InvalidStyle, 69 | InvalidEvent, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /AssParser.Lib/UUEncode.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace AssParser.Lib 4 | { 5 | public class UUEncode 6 | { 7 | private static readonly char[] EncLUT = 8 | { 9 | '!', '"', '#', '$', '%', '&', '\'', '(', 10 | ')', '*', '+', ',', '-', '.', '/', '0', 11 | '1', '2', '3', '4', '5', '6', '7', '8', 12 | '9', ':', ';', '<', '=', '>', '?', '@', 13 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 14 | 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 15 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 16 | 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 17 | }; 18 | /// 19 | /// Use UUEncode to encode byte[] data. Despite being called uuencoding by ass_specs.doc, the format is actually somewhat different from real uuencoding. 20 | /// Please refer to https://github.com/Aegisub/Aegisub/blob/6f546951b4f004da16ce19ba638bf3eedefb9f31/libaegisub/ass/uuencode.cpp for more information. 21 | /// 22 | /// 23 | /// Whether break the line after 80 characters. 24 | /// The linebreak type of source string. True if is CRLF. 25 | /// UUEncoded string. 26 | public static string Encode(byte[] data, bool insertBr = true, bool crlf = true) 27 | { 28 | var written = 0; 29 | var curr = 0; 30 | var resLength = data.Length / 3 * 4 + (data.Length % 3 == 0 ? 0 : data.Length % 3 + 1); 31 | if (insertBr) 32 | { 33 | resLength += (resLength / 80 - (resLength % 80 == 0 ? 1 : 0)) * (crlf ? 2 : 1); 34 | } 35 | var res = new char[resLength]; 36 | var dst = new char[4]; 37 | var length = data.Length; 38 | for (var pos = 0; pos < length; pos += 3) 39 | { 40 | var numBytesRemain = Math.Min(length - pos, 3); 41 | 42 | dst[0] = EncLUT[(data[pos] >> 2) & 0x3f]; 43 | switch (numBytesRemain) 44 | { 45 | case 1: 46 | dst[1] = EncLUT[(data[pos + 0] << 4) & 0x3f]; 47 | break; 48 | case 2: 49 | dst[1] = EncLUT[(data[pos + 0] << 4) & 0x3f | (data[pos + 1] >> 4) & 0x0f]; 50 | dst[2] = EncLUT[(data[pos + 1] << 2) & 0x3f]; 51 | break; 52 | case 3: 53 | dst[1] = EncLUT[(data[pos + 0] << 4) & 0x3f | (data[pos + 1] >> 4) & 0x0f]; 54 | dst[2] = EncLUT[(data[pos + 1] << 2) & 0x3f | (data[pos + 2] >> 6) & 0x03]; 55 | dst[3] = EncLUT[(data[pos + 2] << 0) & 0x3f]; 56 | break; 57 | } 58 | for (var i = 0; i < numBytesRemain + 1; i++) 59 | { 60 | res[curr++] = dst[i]; 61 | written++; 62 | if (insertBr && written == 80 && numBytesRemain == 3) 63 | { 64 | if (crlf) res[curr++] = '\r'; 65 | res[curr++] = '\n'; 66 | written = 0; 67 | } 68 | } 69 | } 70 | return new string(res); 71 | } 72 | /// 73 | /// Use UUEncode to decode byte[] data. Despite being called uuencoding by ass_specs.doc, the format is actually somewhat different from real uuencoding. 74 | /// Please refer to https://github.com/Aegisub/Aegisub/blob/6f546951b4f004da16ce19ba638bf3eedefb9f31/libaegisub/ass/uuencode.cpp for more information. 75 | /// 76 | /// UUEncoded string. 77 | /// The linebreak type of source string. True if is CRLF. 78 | /// UUDecoded byte[]. 79 | public static byte[] Decode(string data, out bool crlf) 80 | { 81 | crlf = false; 82 | var byteData = Encoding.ASCII.GetBytes(data); 83 | var length = byteData.Length; 84 | var curr = 0; 85 | var src = new byte[4]; 86 | var res = new byte[length * 3 / 4]; 87 | for (var pos = 0; pos + 1 < length;) 88 | { 89 | var numBytesRemain = Math.Min(length - pos, 4); 90 | var bytes = 0; 91 | for (var i = 0; i < numBytesRemain; ++pos) 92 | { 93 | var c = byteData[pos]; 94 | if (c != '\n' && c != '\r') 95 | { 96 | src[i++] = (byte)(c - 33); 97 | bytes++; 98 | } 99 | else 100 | { 101 | if (!crlf && c == '\r') crlf = true; 102 | } 103 | } 104 | if (bytes > 1) 105 | res[curr++] = (byte)((src[0] << 2) & 0xff | (src[1] >> 4) & 0x03); 106 | if (bytes > 2) 107 | res[curr++] = (byte)((src[1] << 4) & 0xff | (src[2] >> 2) & 0x0f); 108 | if (bytes > 3) 109 | res[curr++] = (byte)((src[2] << 6) & 0xff | (src[3] >> 0) & 0x3f); 110 | } 111 | Array.Resize(ref res, curr); 112 | return res; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /AssParser.Lib/AssParserExt.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Text; 3 | 4 | namespace AssParser.Lib 5 | { 6 | public static partial class AssParserExt 7 | { 8 | /// 9 | /// Export all used fonts. All used chars are listed in FontDetail.UsedChar, including \h. 10 | /// Italic, Bold and @(vertical alignment) is considered as different font. 11 | /// 12 | /// 13 | /// List of distinct used fonts. 14 | /// If there is any unvalid part. 15 | public static FontDetail[] UsedFonts(this AssSubtitleModel assSubtitle) 16 | { 17 | ConcurrentDictionary> result = new(); 18 | BlockingCollection words = new(); 19 | Dictionary styles = new(); 20 | foreach (var style in assSubtitle.Styles.styles) 21 | { 22 | styles.TryAdd(style.Name, style); 23 | } 24 | Parallel.ForEach(assSubtitle.Events.events, item => 25 | { 26 | var spLeft = item.Text.Split('{').ToList(); 27 | var currentStyle = styles[item.Style]; 28 | if (!item.Text.StartsWith("{")) 29 | { 30 | string text; 31 | if (spLeft == null || spLeft.Count == 0) 32 | { 33 | text = item.Text; 34 | } 35 | else 36 | { 37 | text = spLeft[0]; 38 | spLeft.RemoveAt(0); 39 | } 40 | var word = text.Replace("\\N", "").Replace("\\n", "").Replace("\\h", "\u00A0"); 41 | if (word.Length > 0) 42 | { 43 | var bold = currentStyle.Bold != "0" ? 1 : 0; 44 | var isItalic = currentStyle.Italic != "0"; 45 | var detail = new FontDetail() 46 | { 47 | FontName = currentStyle.Fontname, 48 | UsedChar = word, 49 | Bold = bold, 50 | IsItalic = isItalic 51 | }; 52 | var charDir = result.GetOrAdd(detail, new ConcurrentDictionary()); 53 | foreach (var c in word) 54 | { 55 | charDir.TryAdd(c, 0); 56 | } 57 | } 58 | } 59 | if (spLeft == null) 60 | { 61 | return; 62 | } 63 | foreach (var s in spLeft) 64 | { 65 | var spRight = s.Split('}'); 66 | if (spRight.Length > 0) 67 | { 68 | var tags = spRight[0].Split("\\"); 69 | foreach (var t in tags) 70 | { 71 | if (t.Length == 0) 72 | { 73 | continue; 74 | } 75 | switch (t[0]) 76 | { 77 | case 'f': 78 | if (t.Length > 2 && t[1] == 'n') 79 | { 80 | currentStyle.Fontname = t[2..]; 81 | } 82 | break; 83 | case 'b': 84 | if (t.Length == 2 || t.Length == 4) 85 | { 86 | if (int.TryParse(t[1..], out var weight)) 87 | { 88 | currentStyle.Bold = weight.ToString(); 89 | } 90 | } 91 | break; 92 | case 'i': 93 | if (t.Length == 2) 94 | { 95 | currentStyle.Italic = t[1..]; 96 | } 97 | break; 98 | case 'r': 99 | if (t.Length == 1) 100 | { 101 | currentStyle = styles[item.Style]; 102 | } 103 | else if (t.Length > 1) 104 | { 105 | if (!styles.ContainsKey(t[1..])) 106 | { 107 | throw new Exception($"Style {t} not found"); 108 | } 109 | currentStyle = styles[t[1..]]; 110 | } 111 | break; 112 | default: 113 | break; 114 | } 115 | } 116 | if (spRight.Length > 1) 117 | { 118 | var word = spRight[1].Replace("\\N", "").Replace("\\n", "").Replace("\\h", "\u00A0"); 119 | if (word.Length > 0) 120 | { 121 | var bold = Convert.ToInt32(currentStyle.Bold); 122 | bold = bold == -1 ? 1 : bold; 123 | var isItalic = currentStyle.Italic != "0"; 124 | var detail = new FontDetail() 125 | { 126 | FontName = currentStyle.Fontname, 127 | UsedChar = word, 128 | Bold = bold, 129 | IsItalic = isItalic 130 | }; 131 | var charDir = result.GetOrAdd(detail, new ConcurrentDictionary()); 132 | foreach (var c in word) 133 | { 134 | charDir.TryAdd(c, 0); 135 | } 136 | } 137 | } 138 | } 139 | } 140 | }); 141 | var fonts = new FontDetail[result.Count]; 142 | int i = 0; 143 | foreach (var s in result) 144 | { 145 | var sb = new StringBuilder(); 146 | foreach (var c in s.Value) 147 | { 148 | sb.Append(c.Key); 149 | } 150 | s.Key.UsedChar = sb.ToString(); 151 | fonts[i++] = s.Key; 152 | } 153 | return fonts; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /AssParser.Lib/AssSubtitleModel.cs: -------------------------------------------------------------------------------- 1 | namespace AssParser.Lib 2 | { 3 | public class AssSubtitleModel 4 | { 5 | public ScriptInfo ScriptInfo { get; set; } = new(); 6 | public Styles Styles { get; set; } = new(); 7 | public Events Events { get; set; } = new(); 8 | public List Ord { get; set; } = new(); 9 | public Dictionary UnknownSections { get; set; } = new(); 10 | public override string ToString() 11 | { 12 | using MemoryStream stream = new(); 13 | using StreamWriter writer = new(stream, leaveOpen: true); 14 | AssParser.WriteToStreamAsync(this, writer).Wait(); 15 | stream.Position = 0; 16 | using StreamReader reader = new(stream, leaveOpen: true); 17 | return reader.ReadToEnd(); 18 | } 19 | } 20 | 21 | public class ScriptInfo 22 | { 23 | public ScriptInfo() 24 | { 25 | SciptInfoItems = new(); 26 | } 27 | public Dictionary SciptInfoItems; 28 | public string? Title 29 | { 30 | set 31 | { 32 | SciptInfoItems["Title"] = value; 33 | } 34 | get 35 | { 36 | if (SciptInfoItems.TryGetValue("Title", out var item)) 37 | { 38 | return item; 39 | } 40 | else 41 | { 42 | return null; 43 | } 44 | } 45 | } 46 | public string? ScriptType 47 | { 48 | set 49 | { 50 | SciptInfoItems["ScriptType"] = value; 51 | } 52 | get 53 | { 54 | if (SciptInfoItems.TryGetValue("ScriptType", out var item)) 55 | { 56 | return item; 57 | } 58 | else 59 | { 60 | return null; 61 | } 62 | } 63 | } 64 | public int? WrapStyle 65 | { 66 | set 67 | { 68 | SciptInfoItems["WrapStyle"] = value.ToString(); 69 | } 70 | get 71 | { 72 | if (SciptInfoItems.TryGetValue("WrapStyle", out var item)) 73 | { 74 | return Convert.ToInt32(item); 75 | } 76 | else 77 | { 78 | return null; 79 | } 80 | } 81 | } 82 | public string? ScaledBorderAndShadow 83 | { 84 | set 85 | { 86 | SciptInfoItems["ScaledBorderAndShadow"] = value; 87 | } 88 | get 89 | { 90 | if (SciptInfoItems.TryGetValue("ScaledBorderAndShadow", out var item)) 91 | { 92 | return item; 93 | } 94 | else 95 | { 96 | return null; 97 | } 98 | } 99 | } 100 | public string? YCbCrMatrix 101 | { 102 | set 103 | { 104 | SciptInfoItems["YCbCrMatrix"] = value; 105 | } 106 | get 107 | { 108 | if (SciptInfoItems.TryGetValue("YCbCrMatrix", out var item)) 109 | { 110 | return item; 111 | } 112 | else 113 | { 114 | return null; 115 | } 116 | } 117 | } 118 | public int? PlayResX 119 | { 120 | set 121 | { 122 | SciptInfoItems["PlayResX"] = value.ToString(); 123 | } 124 | get 125 | { 126 | if (SciptInfoItems.TryGetValue("PlayResX", out var item)) 127 | { 128 | return Convert.ToInt32(item); 129 | } 130 | else 131 | { 132 | return null; 133 | } 134 | } 135 | } 136 | public int? PlayResY 137 | { 138 | set 139 | { 140 | SciptInfoItems["PlayResY"] = value.ToString(); 141 | } 142 | get 143 | { 144 | if (SciptInfoItems.TryGetValue("PlayResY", out var item)) 145 | { 146 | return Convert.ToInt32(item); 147 | } 148 | else 149 | { 150 | return null; 151 | } 152 | } 153 | } 154 | } 155 | public class Styles 156 | { 157 | public string[] Format; 158 | public List