├── 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 · [](https://github.com/AmusementClub/AssParser/actions/workflows/dotnet-nuget.yml)  [](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