├── .gitattributes
├── .github
└── workflows
│ └── afs-cmd-ci.yml
├── .gitignore
├── AssFontSubset.Avalonia
├── App.axaml
├── App.axaml.cs
├── AssFontSubset.Avalonia.csproj
├── Assets
│ └── avalonia-logo.ico
├── I18n
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ └── Resources.zh-hans.resx
├── Program.cs
├── ViewModels
│ ├── MainWindowViewModel.cs
│ └── ViewModelBase.cs
├── Views
│ ├── MainWindow.axaml
│ └── MainWindow.axaml.cs
└── app.manifest
├── AssFontSubset.Console
├── AssFontSubset.Console.csproj
└── Program.cs
├── AssFontSubset.Core
├── AssFontSubset.Core.csproj
└── src
│ ├── AssFont.cs
│ ├── FontConstant.cs
│ ├── FontParse.cs
│ ├── HarfBuzzSubset.cs
│ ├── PyFontTools.cs
│ ├── SubsetConfig.cs
│ ├── SubsetCore.cs
│ ├── SubsetFont.cs
│ └── SubsetToolBase.cs
├── AssFontSubset.CoreTests
├── AssFontSubset.Core.Tests.csproj
└── src
│ └── AssFontTests.cs
├── AssFontSubset.sln
├── AssFontSubset.slnx
├── HarfBuzzBinding
├── HarfBuzzBinding.csproj
├── README.md
└── src
│ ├── Methods.cs
│ └── Native
│ ├── Apis.cs
│ ├── Library.cs
│ ├── NativeTypeNameAttribute.cs
│ └── Subset
│ ├── Apis.cs
│ ├── hb_subset_flags_t.cs
│ └── hb_subset_sets_t.cs
├── README.md
├── README_v1.md
├── build
└── global.props
└── nuget.config
/.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 |
--------------------------------------------------------------------------------
/.github/workflows/afs-cmd-ci.yml:
--------------------------------------------------------------------------------
1 | name: Build afs.cmd
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | paths:
7 | - 'AssFontSubset.Core/**.cs'
8 | - 'AssFontSubset.Core/**.csproj'
9 | - 'AssFontSubset.Console/**.cs'
10 | - 'AssFontSubset.Console/**.csproj'
11 | pull_request:
12 | branches: [ "master" ]
13 | paths:
14 | - 'AssFontSubset.Core/**.cs'
15 | - 'AssFontSubset.Core/**.csproj'
16 | - 'AssFontSubset.Console/**.cs'
17 | - 'AssFontSubset.Console/**.csproj'
18 | workflow_dispatch:
19 |
20 | jobs:
21 | build:
22 | name: build-${{ matrix.config.target }}-${{ matrix.arch }}
23 | runs-on: ${{ matrix.config.os }}
24 | env:
25 | identifier: ${{ matrix.config.target }}-${{ matrix.arch }}
26 | strategy:
27 | matrix:
28 | harfbuzz_ver: ['10.1.0']
29 | dotnet_version: ['9.x']
30 | config:
31 | - os: windows-latest
32 | target: win
33 | framework: net9.0
34 | - os: windows-latest
35 | target: linux-musl
36 | framework: net8.0 # need zig fix? https://github.com/ziglang/zig/pull/20081
37 | - os: macos-latest
38 | target: osx
39 | framework: net9.0
40 | arch: [x64, arm64]
41 |
42 | steps:
43 |
44 | - name: Setup .NET
45 | uses: actions/setup-dotnet@v4
46 | with:
47 | dotnet-version: ${{ matrix.dotnet_version }}
48 |
49 | - name: Checkout
50 | uses: actions/checkout@v4
51 |
52 | - name: Test
53 | if: matrix.config.target != 'osx'
54 | run: |
55 | dotnet --version
56 | cd ./AssFontSubset.CoreTests
57 | dotnet restore
58 | dotnet test
59 | cd ..
60 |
61 | - name: Setup Zig Compiler (target linux)
62 | if: matrix.config.target == 'linux-musl'
63 | uses: mlugg/setup-zig@v1
64 | with:
65 | version: "master"
66 |
67 | - name: Add llvm-objcopy (target linux)
68 | if: matrix.config.target == 'linux-musl'
69 | run: |
70 | Invoke-WebRequest -Uri https://github.com/MIRIMIRIM/build-harfbuzz/releases/download/utils/llvm-objcopy.exe -OutFile ./AssFontSubset.Console/llvm-objcopy.exe
71 |
72 | - name: Change to afs.cmd and Publish
73 | run: |
74 | cd ./AssFontSubset.Console
75 | dotnet restore
76 | dotnet publish -c Release -r ${{ env.identifier }} -f ${{ matrix.config.framework }}
77 |
78 | - name: Set short version
79 | shell: bash
80 | run: |
81 | ver_short=`git rev-parse --short HEAD`
82 | echo "VERSION=$ver_short" >> $GITHUB_ENV
83 |
84 | - name: Upload exe files
85 | uses: actions/upload-artifact@v4
86 | with:
87 | name: AssFontSubset.Console_g${{ env.VERSION }}_${{ env.identifier }}
88 | path: |
89 | AssFontSubset.Console/bin/Release/${{ matrix.config.framework }}/${{ env.identifier }}/publish/
90 | !AssFontSubset.Console/bin/Release/${{ matrix.config.framework }}/${{ env.identifier }}/publish/*.a
91 | !AssFontSubset.Console/bin/Release/${{ matrix.config.framework }}/${{ env.identifier }}/publish/*.pdb
92 | !AssFontSubset.Console/bin/Release/${{ matrix.config.framework }}/${{ env.identifier }}/publish/*.dbg
93 | !AssFontSubset.Console/bin/Release/${{ matrix.config.framework }}/${{ env.identifier }}/publish/*.dwarf
94 | !AssFontSubset.Console/bin/Release/${{ matrix.config.framework }}/${{ env.identifier }}/publish/*.dSYM
95 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 | build/*.7z
25 | build/*.bat
26 | build/*.zip
27 |
28 | # Visual Studio 2015 cache/options directory
29 | .vs/
30 | # Uncomment if you have tasks that create the project's static files in wwwroot
31 | #wwwroot/
32 |
33 | # MSTest test Results
34 | [Tt]est[Rr]esult*/
35 | [Bb]uild[Ll]og.*
36 |
37 | # NUNIT
38 | *.VisualState.xml
39 | TestResult.xml
40 |
41 | # Build Results of an ATL Project
42 | [Dd]ebugPS/
43 | [Rr]eleasePS/
44 | dlldata.c
45 |
46 | # DNX
47 | project.lock.json
48 | project.fragment.lock.json
49 | artifacts/
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # NCrunch
117 | _NCrunch_*
118 | .*crunch*.local.xml
119 | nCrunchTemp_*
120 |
121 | # MightyMoose
122 | *.mm.*
123 | AutoTest.Net/
124 |
125 | # Web workbench (sass)
126 | .sass-cache/
127 |
128 | # Installshield output folder
129 | [Ee]xpress/
130 |
131 | # DocProject is a documentation generator add-in
132 | DocProject/buildhelp/
133 | DocProject/Help/*.HxT
134 | DocProject/Help/*.HxC
135 | DocProject/Help/*.hhc
136 | DocProject/Help/*.hhk
137 | DocProject/Help/*.hhp
138 | DocProject/Help/Html2
139 | DocProject/Help/html
140 |
141 | # Click-Once directory
142 | publish/
143 |
144 | # Publish Web Output
145 | *.[Pp]ublish.xml
146 | *.azurePubxml
147 | # TODO: Comment the next line if you want to checkin your web deploy settings
148 | # but database connection strings (with potential passwords) will be unencrypted
149 | #*.pubxml
150 | *.publishproj
151 |
152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
153 | # checkin your Azure Web App publish settings, but sensitive information contained
154 | # in these scripts will be unencrypted
155 | PublishScripts/
156 |
157 | # NuGet Packages
158 | *.nupkg
159 | # The packages folder can be ignored because of Package Restore
160 | **/packages/*
161 | # except build/, which is used as an MSBuild target.
162 | !**/packages/build/
163 | # Uncomment if necessary however generally it will be regenerated when needed
164 | #!**/packages/repositories.config
165 | # NuGet v3's project.json files produces more ignoreable files
166 | *.nuget.props
167 | *.nuget.targets
168 |
169 | # Microsoft Azure Build Output
170 | csx/
171 | *.build.csdef
172 |
173 | # Microsoft Azure Emulator
174 | ecf/
175 | rcf/
176 |
177 | # Windows Store app package directories and files
178 | AppPackages/
179 | BundleArtifacts/
180 | Package.StoreAssociation.xml
181 | _pkginfo.txt
182 |
183 | # Visual Studio cache files
184 | # files ending in .cache can be ignored
185 | *.[Cc]ache
186 | # but keep track of directories ending in .cache
187 | !*.[Cc]ache/
188 |
189 | # Others
190 | ClientBin/
191 | ~$*
192 | *~
193 | *.dbmdl
194 | *.dbproj.schemaview
195 | *.jfm
196 | *.pfx
197 | *.publishsettings
198 | node_modules/
199 | orleans.codegen.cs
200 |
201 | # Since there are multiple workflows, uncomment next line to ignore bower_components
202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
203 | #bower_components/
204 |
205 | # RIA/Silverlight projects
206 | Generated_Code/
207 |
208 | # Backup & report files from converting an old project file
209 | # to a newer Visual Studio version. Backup files are not needed,
210 | # because we have git ;-)
211 | _UpgradeReport_Files/
212 | Backup*/
213 | UpgradeLog*.XML
214 | UpgradeLog*.htm
215 |
216 | # SQL Server files
217 | *.mdf
218 | *.ldf
219 |
220 | # Business Intelligence projects
221 | *.rdl.data
222 | *.bim.layout
223 | *.bim_*.settings
224 |
225 | # Microsoft Fakes
226 | FakesAssemblies/
227 |
228 | # GhostDoc plugin setting file
229 | *.GhostDoc.xml
230 |
231 | # Node.js Tools for Visual Studio
232 | .ntvs_analysis.dat
233 |
234 | # Visual Studio 6 build log
235 | *.plg
236 |
237 | # Visual Studio 6 workspace options file
238 | *.opt
239 |
240 | # Visual Studio LightSwitch build output
241 | **/*.HTMLClient/GeneratedArtifacts
242 | **/*.DesktopClient/GeneratedArtifacts
243 | **/*.DesktopClient/ModelManifest.xml
244 | **/*.Server/GeneratedArtifacts
245 | **/*.Server/ModelManifest.xml
246 | _Pvt_Extensions
247 |
248 | # Paket dependency manager
249 | .paket/paket.exe
250 | paket-files/
251 |
252 | # FAKE - F# Make
253 | .fake/
254 |
255 | # JetBrains Rider
256 | .idea/
257 | *.sln.iml
258 |
259 | # CodeRush
260 | .cr/
261 |
262 | # Python Tools for Visual Studio (PTVS)
263 | __pycache__/
264 | *.pyc
265 |
266 | # Test Files
267 | test/
268 | */Properties/*
269 | /native
270 |
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/App.axaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using AssFontSubset.Avalonia.ViewModels;
2 | using AssFontSubset.Avalonia.Views;
3 | using Avalonia;
4 | using Avalonia.Controls.ApplicationLifetimes;
5 | using Avalonia.Data.Core;
6 | using Avalonia.Data.Core.Plugins;
7 | using Avalonia.Markup.Xaml;
8 |
9 | namespace AssFontSubset.Avalonia
10 | {
11 | public partial class App : Application
12 | {
13 | public override void Initialize()
14 | {
15 | AvaloniaXamlLoader.Load(this);
16 | }
17 |
18 | public override void OnFrameworkInitializationCompleted()
19 | {
20 | I18n.Resources.Culture = System.Globalization.CultureInfo.CurrentUICulture;
21 |
22 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
23 | {
24 | // Line below is needed to remove Avalonia data validation.
25 | // Without this line you will get duplicate validations from both Avalonia and CT
26 | BindingPlugins.DataValidators.RemoveAt(0);
27 | desktop.MainWindow = new MainWindow
28 | {
29 | DataContext = new MainWindowViewModel(),
30 | };
31 | }
32 |
33 | base.OnFrameworkInitializationCompleted();
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/AssFontSubset.Avalonia.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | true
5 | app.manifest
6 | true
7 | False
8 |
9 |
10 | False
11 |
12 |
13 | False
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | PublicResXFileCodeGenerator
39 | Resources.Designer.cs
40 |
41 |
42 |
43 |
44 |
45 | True
46 | True
47 | Resources.resx
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/Assets/avalonia-logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AmusementClub/AssFontSubset/1f64e7d4285e4211c9593c66e00b278b7d9e53aa/AssFontSubset.Avalonia/Assets/avalonia-logo.ico
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/I18n/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | //
5 | // Changes to this file may cause incorrect behavior and will be lost if
6 | // the code is regenerated.
7 | //
8 | //------------------------------------------------------------------------------
9 |
10 | namespace AssFontSubset.Avalonia.I18n {
11 | using System;
12 |
13 |
14 | ///
15 | /// A strongly-typed resource class, for looking up localized strings, etc.
16 | ///
17 | // This class was auto-generated by the StronglyTypedResourceBuilder
18 | // class via a tool like ResGen or Visual Studio.
19 | // To add or remove a member, edit your .ResX file then rerun ResGen
20 | // with the /str option, or rebuild your VS project.
21 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
22 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
23 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
24 | public class Resources {
25 |
26 | private static global::System.Resources.ResourceManager resourceMan;
27 |
28 | private static global::System.Globalization.CultureInfo resourceCulture;
29 |
30 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
31 | internal Resources() {
32 | }
33 |
34 | ///
35 | /// Returns the cached ResourceManager instance used by this class.
36 | ///
37 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
38 | public static global::System.Resources.ResourceManager ResourceManager {
39 | get {
40 | if (object.ReferenceEquals(resourceMan, null)) {
41 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AssFontSubset.Avalonia.I18n.Resources", typeof(Resources).Assembly);
42 | resourceMan = temp;
43 | }
44 | return resourceMan;
45 | }
46 | }
47 |
48 | ///
49 | /// Overrides the current thread's CurrentUICulture property for all
50 | /// resource lookups using this strongly typed resource class.
51 | ///
52 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
53 | public static global::System.Globalization.CultureInfo Culture {
54 | get {
55 | return resourceCulture;
56 | }
57 | set {
58 | resourceCulture = value;
59 | }
60 | }
61 |
62 | ///
63 | /// Looks up a localized string similar to HarfBuzz-Subset.
64 | ///
65 | public static string BackendHarfbuzzSubset {
66 | get {
67 | return ResourceManager.GetString("BackendHarfbuzzSubset", resourceCulture);
68 | }
69 | }
70 |
71 | ///
72 | /// Looks up a localized string similar to Use Harfbuzz-Subset subset fonts..
73 | ///
74 | public static string BackendHarfbuzzSubsetTip {
75 | get {
76 | return ResourceManager.GetString("BackendHarfbuzzSubsetTip", resourceCulture);
77 | }
78 | }
79 |
80 | ///
81 | /// Looks up a localized string similar to Clear.
82 | ///
83 | public static string ClearInput {
84 | get {
85 | return ResourceManager.GetString("ClearInput", resourceCulture);
86 | }
87 | }
88 |
89 | ///
90 | /// Looks up a localized string similar to Debug.
91 | ///
92 | public static string DebugOption {
93 | get {
94 | return ResourceManager.GetString("DebugOption", resourceCulture);
95 | }
96 | }
97 |
98 | ///
99 | /// Looks up a localized string similar to Keep subset temp files when enabled..
100 | ///
101 | public static string DebugOptionTip {
102 | get {
103 | return ResourceManager.GetString("DebugOptionTip", resourceCulture);
104 | }
105 | }
106 |
107 | ///
108 | /// Looks up a localized string similar to No ass files. Please check!.
109 | ///
110 | public static string ErrorNoAssFile {
111 | get {
112 | return ResourceManager.GetString("ErrorNoAssFile", resourceCulture);
113 | }
114 | }
115 |
116 | ///
117 | /// Looks up a localized string similar to ASS Subtitles.
118 | ///
119 | public static string InputAssFiles {
120 | get {
121 | return ResourceManager.GetString("InputAssFiles", resourceCulture);
122 | }
123 | }
124 |
125 | ///
126 | /// Looks up a localized string similar to Font Folder.
127 | ///
128 | public static string InputFontFolder {
129 | get {
130 | return ResourceManager.GetString("InputFontFolder", resourceCulture);
131 | }
132 | }
133 |
134 | ///
135 | /// Looks up a localized string similar to Output Folder.
136 | ///
137 | public static string OutputFolder {
138 | get {
139 | return ResourceManager.GetString("OutputFolder", resourceCulture);
140 | }
141 | }
142 |
143 | ///
144 | /// Looks up a localized string similar to SourceHan Ellipsis.
145 | ///
146 | public static string SourceHanEllipsis {
147 | get {
148 | return ResourceManager.GetString("SourceHanEllipsis", resourceCulture);
149 | }
150 | }
151 |
152 | ///
153 | /// Looks up a localized string similar to A special hack. The ellipses of Source Han fonts will be aligned in the center when enabled..
154 | ///
155 | public static string SourceHanEllipsisTip {
156 | get {
157 | return ResourceManager.GetString("SourceHanEllipsisTip", resourceCulture);
158 | }
159 | }
160 |
161 | ///
162 | /// Looks up a localized string similar to Start.
163 | ///
164 | public static string StartButton {
165 | get {
166 | return ResourceManager.GetString("StartButton", resourceCulture);
167 | }
168 | }
169 |
170 | ///
171 | /// Looks up a localized string similar to Subset.
172 | ///
173 | public static string Subset {
174 | get {
175 | return ResourceManager.GetString("Subset", resourceCulture);
176 | }
177 | }
178 |
179 | ///
180 | /// Looks up a localized string similar to Subset Completed. Please check Output Folder..
181 | ///
182 | public static string SuccessSubset {
183 | get {
184 | return ResourceManager.GetString("SuccessSubset", resourceCulture);
185 | }
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/I18n/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | text/microsoft-resx
11 |
12 |
13 | 1.3
14 |
15 |
16 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
17 |
18 |
19 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
20 |
21 |
22 | Subset
23 |
24 |
25 | ASS Subtitles
26 |
27 |
28 | Font Folder
29 |
30 |
31 | Output Folder
32 |
33 |
34 | Start
35 |
36 |
37 | Clear
38 |
39 |
40 | SourceHan Ellipsis
41 |
42 |
43 | A special hack. The ellipses of Source Han fonts will be aligned in the center when enabled.
44 |
45 |
46 | Debug
47 |
48 |
49 | Keep subset temp files when enabled.
50 |
51 |
52 | HarfBuzz-Subset
53 |
54 |
55 | No ass files. Please check!
56 |
57 |
58 | Subset Completed. Please check Output Folder.
59 |
60 |
61 | Use Harfbuzz-Subset subset fonts.
62 |
63 |
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/I18n/Resources.zh-hans.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 | text/microsoft-resx
4 |
5 |
6 | 1.3
7 |
8 |
9 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
10 |
11 |
12 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
13 |
14 |
15 | 子集化
16 |
17 |
18 | 字幕文件
19 |
20 |
21 | 字体目录
22 |
23 |
24 | 输出目录
25 |
26 |
27 | 开 始
28 |
29 |
30 | 清空
31 |
32 |
33 | 居中思源省略号
34 |
35 |
36 | A special hack. 打开后,思源黑体和宋体的省略号会被居中对齐。
37 |
38 |
39 | 调试选项
40 |
41 |
42 | 打开后会保留各种临时文件,用于检查字体名字等在各个阶段是否正确。
43 |
44 |
45 | 没有 ASS 文件可供处理,请检查
46 |
47 |
48 | 子集化完成,请检查 output 文件夹
49 |
50 |
51 | 使用 Harfbuzz-Subset 子集化字体。
52 |
53 |
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Logging;
3 | using System;
4 |
5 | namespace AssFontSubset.Avalonia
6 | {
7 | internal sealed class Program
8 | {
9 | // Initialization code. Don't use any Avalonia, third-party APIs or any
10 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
11 | // yet and stuff might break.
12 | [STAThread]
13 | public static void Main(string[] args) => BuildAvaloniaApp()
14 | .StartWithClassicDesktopLifetime(args);
15 |
16 | // Avalonia configuration, don't remove; also used by visual designer.
17 | public static AppBuilder BuildAvaloniaApp()
18 | => AppBuilder.Configure()
19 | .UsePlatformDetect()
20 | .With(new AvaloniaNativePlatformOptions { RenderingMode = [AvaloniaNativeRenderingMode.Software] })
21 | .WithInterFont()
22 | .LogToTrace();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/ViewModels/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | namespace AssFontSubset.Avalonia.ViewModels;
4 |
5 | public partial class MainWindowViewModel : ViewModelBase
6 | {
7 | public static string WindowTitle => $"AssFontSubset v{Assembly.GetEntryAssembly()!.GetName().Version}";
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/ViewModels/ViewModelBase.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.ComponentModel;
2 |
3 | namespace AssFontSubset.Avalonia.ViewModels
4 | {
5 | public class ViewModelBase : ObservableObject
6 | {
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/Views/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
27 |
28 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
38 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/Views/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Input;
3 | using Avalonia.Interactivity;
4 | using Avalonia.Platform.Storage;
5 | using System.Collections.Generic;
6 | using System.IO;
7 | using System.Linq;
8 | using AssFontSubset.Core;
9 | using System;
10 | using MsBox.Avalonia.Enums;
11 | using MsBox.Avalonia;
12 | using System.Threading.Tasks;
13 | using MsBox.Avalonia.Base;
14 | using System.ComponentModel;
15 | using AssFontSubset.Avalonia.ViewModels;
16 | using I18nResources = AssFontSubset.Avalonia.I18n.Resources;
17 |
18 | namespace AssFontSubset.Avalonia.Views
19 | {
20 | public partial class MainWindow : Window, INotifyPropertyChanged
21 | {
22 | public MainWindow()
23 | {
24 | InitializeComponent();
25 | DataContext = new MainWindowViewModel();
26 | AddHandler(DragDrop.DropEvent, Drop_Files);
27 | }
28 |
29 | private void Clear_Click(object sender, RoutedEventArgs e)
30 | {
31 | AssFileList.ItemsSource = null;
32 | FontFolder.Text = string.Empty;
33 | OutputFolder.Text = string.Empty;
34 | }
35 |
36 | private async void Start_Click(object sender, RoutedEventArgs e)
37 | {
38 | var sourceHanEllipsis = this.FindControl("SourceHanEllipsis")!.IsChecked!.Value;
39 | var debugMode = this.FindControl("Debug")!.IsChecked!.Value;
40 | var useHbSubset = this.FindControl("UseHbSubset")!.IsChecked!.Value;
41 |
42 | if (AssFileList.Items.Count == 0)
43 | {
44 | await ShowMessageBox("Error", I18nResources.ErrorNoAssFile);
45 | return;
46 | }
47 | var path = new FileInfo[AssFileList.Items.Count];
48 | for (int i = 0; i < path.Length; i++)
49 | {
50 | path[i] = new FileInfo((string)AssFileList.Items.GetAt(i)!);
51 | }
52 | var fontPath = new DirectoryInfo(FontFolder.Text!);
53 | var outputPath = new DirectoryInfo(OutputFolder.Text!);
54 | DirectoryInfo? binPath = null;
55 |
56 | var subsetConfig = new SubsetConfig
57 | {
58 | SourceHanEllipsis = sourceHanEllipsis,
59 | DebugMode = debugMode,
60 | Backend = useHbSubset ? SubsetBackend.HarfBuzzSubset : SubsetBackend.PyFontTools,
61 | };
62 |
63 | await AssFontSubsetByPyFT(path, fontPath, outputPath, binPath, subsetConfig);
64 | }
65 |
66 | private async Task AssFontSubsetByPyFT(FileInfo[] path, DirectoryInfo? fontPath, DirectoryInfo? outputPath, DirectoryInfo? binPath, SubsetConfig subsetConfig)
67 | {
68 | try
69 | {
70 | Progressing.IsIndeterminate = true;
71 | var ssFt = new SubsetCore();
72 | await ssFt.SubsetAsync(path, fontPath, outputPath, binPath, subsetConfig);
73 | Progressing.IsIndeterminate = false;
74 | await ShowMessageBox("Success", I18nResources.SuccessSubset);
75 | }
76 | catch (Exception ex)
77 | {
78 | Progressing.IsIndeterminate = false;
79 | await ShowMessageBox("Error", ex.Message);
80 | }
81 | }
82 |
83 | private async Task ShowMessageBox(string title, string message)
84 | {
85 | var box = MessageBoxManager.GetMessageBoxStandard(title, message, ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.None, WindowStartupLocation.CenterOwner);
86 | await box.ShowWindowDialogAsync(this);
87 | }
88 |
89 | private void Drop_Files(object? sender, DragEventArgs e)
90 | {
91 | var dragData = (IEnumerable?)e.Data.Get(DataFormats.Files);
92 | if (dragData == null) return;
93 | var files = dragData.ToArray();
94 |
95 | var validFiles = files.Where(f => Path.GetExtension(f.Name) == ".ass").ToArray();
96 | if (validFiles.Length == 0) return;
97 |
98 | AssFileList.ItemsSource = validFiles.Select(f => f.Path.LocalPath).Order().ToList();
99 | var dir = validFiles[0].GetParentAsync().Result;
100 | FontFolder.Text = Path.Combine(dir!.Path.LocalPath, "fonts");
101 | OutputFolder.Text = Path.Combine(dir!.Path.LocalPath, "output");
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/AssFontSubset.Avalonia/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/AssFontSubset.Console/AssFontSubset.Console.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | enable
6 | True
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/AssFontSubset.Console/Program.cs:
--------------------------------------------------------------------------------
1 | using AssFontSubset.Core;
2 | using System.CommandLine;
3 | using Microsoft.Extensions.Logging;
4 | using ZLogger;
5 |
6 | namespace AssFontSubset.Console;
7 |
8 | internal static class Program
9 | {
10 | static async Task Main(string[] args)
11 | {
12 | var path = new CliArgument("path")
13 | {
14 | Description = "要子集化的 ASS 字幕文件路径,可以输入多个同目录的字幕文件"
15 | };
16 | var fontPath = new CliOption("--fonts")
17 | {
18 | Description = "ASS 字幕文件需要的字体所在目录,默认为 ASS 同目录的 fonts 文件夹"
19 | };
20 | var outputPath = new CliOption("--output")
21 | {
22 | Description = "子集化后成品所在目录,默认为 ASS 同目录的 output 文件夹"
23 | };
24 | var subsetBackend = new CliOption("--subset-backend")
25 | {
26 | Description = "子集化使用的后端",
27 | DefaultValueFactory = _ => SubsetBackend.PyFontTools,
28 | };
29 | var binPath = new CliOption("--bin-path")
30 | {
31 | Description = "指定 pyftsubset 和 ttx 所在目录。若未指定,会使用环境变量中的"
32 | };
33 | var sourceHanEllipsis = new CliOption("--source-han-ellipsis")
34 | {
35 | Description = "使思源黑体和宋体的省略号居中对齐",
36 | DefaultValueFactory = _ => true,
37 | };
38 | var debug = new CliOption("--debug")
39 | {
40 | Description = "保留子集化期间的各种临时文件,位于 --output-dir 指定的文件夹;同时打印出所有运行的命令",
41 | DefaultValueFactory = _ => false,
42 | };
43 |
44 | var rootCommand = new CliRootCommand("使用 fonttools 或 harfbuzz-subset 生成 ASS 字幕文件的字体子集,并自动修改字体名称及 ASS 文件中对应的字体名称")
45 | {
46 | path, fontPath, outputPath, subsetBackend, binPath, sourceHanEllipsis, debug
47 | };
48 |
49 | rootCommand.SetAction(async (result, _) =>
50 | {
51 | await Subset(
52 | result.GetValue(path)!,
53 | result.GetValue(fontPath),
54 | result.GetValue(outputPath),
55 | result.GetValue(subsetBackend),
56 | result.GetValue(binPath),
57 | result.GetValue(sourceHanEllipsis),
58 | result.GetValue(debug)
59 | );
60 | });
61 | var config = new CliConfiguration(rootCommand)
62 | {
63 | EnableDefaultExceptionHandler = false,
64 | };
65 |
66 | int exitCode;
67 | try
68 | {
69 | exitCode = await rootCommand.Parse(args, config).InvokeAsync();
70 | }
71 | catch (Exception)
72 | {
73 | exitCode = 1;
74 | }
75 |
76 | if (System.Console.IsOutputRedirected || System.Console.IsErrorRedirected) return exitCode;
77 | System.Console.WriteLine("Press Any key to exit...");
78 | System.Console.ReadKey();
79 |
80 | return exitCode;
81 | }
82 |
83 | static async Task Subset(FileInfo[] path, DirectoryInfo? fontPath, DirectoryInfo? outputPath, SubsetBackend subsetBackend, DirectoryInfo? binPath, bool sourceHanEllipsis, bool debug)
84 | {
85 | var subsetConfig = new SubsetConfig
86 | {
87 | SourceHanEllipsis = sourceHanEllipsis,
88 | DebugMode = debug,
89 | Backend = subsetBackend,
90 | };
91 | var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
92 |
93 | using var factory = LoggerFactory.Create(logging =>
94 | {
95 | logging.SetMinimumLevel(logLevel);
96 | logging.AddZLoggerConsole(options =>
97 | {
98 | options.UsePlainTextFormatter(formatter =>
99 | {
100 | formatter.SetPrefixFormatter($"{0}{1:yyyy-MM-dd'T'HH:mm:sszzz}|{2:short}|", (in MessageTemplate template, in LogInfo info) =>
101 | {
102 | // \u001b[31m => Red(ANSI Escape Code)
103 | // \u001b[0m => Reset
104 | var escapeSequence = info.LogLevel switch
105 | {
106 | LogLevel.Warning => "\u001b[33m",
107 | > LogLevel.Warning => "\u001b[31m",
108 | _ => "\u001b[0m",
109 | };
110 |
111 | template.Format(escapeSequence, info.Timestamp, info.LogLevel);
112 | });
113 | });
114 | options.LogToStandardErrorThreshold = LogLevel.Warning;
115 | });
116 | });
117 | var logger = factory.CreateLogger("AssFontSubset.Console");
118 |
119 | if (path.Length == 0)
120 | {
121 | logger.ZLogError($"Please input ass files\u001b[0m");
122 | throw new ArgumentException();
123 | }
124 |
125 | var ssFt = new SubsetCore(logger);
126 | try
127 | {
128 | await ssFt.SubsetAsync(path, fontPath, outputPath, binPath, subsetConfig);
129 | }
130 | catch (Exception ex)
131 | {
132 | logger.ZLogError($"{ex.Message}\u001b[0m");
133 | throw;
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/AssFontSubset.Core/AssFontSubset.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | enable
5 | true
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/AssFontSubset.Core/src/AssFont.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using Mobsub.SubtitleParse;
3 | using Mobsub.SubtitleParse.AssTypes;
4 | using Mobsub.SubtitleParse.AssUtils;
5 | using System.Text;
6 | using ZLogger;
7 |
8 | namespace AssFontSubset.Core;
9 |
10 | public class AssFont
11 | {
12 | public static Dictionary> GetAssFonts(string file, out AssData ass, ILogger? logger = null)
13 | {
14 | ass = new AssData(logger);
15 | ass.ReadAssFile(file);
16 |
17 | var anlz = new AssAnalyze(ass, logger);
18 | var undefinedStylesTemp = anlz.GetUndefinedStyles();
19 | HashSet undefinedStyles = [];
20 | foreach (var und in undefinedStylesTemp)
21 | {
22 | if (und.StartsWith('*') && ass.Styles.Names.Contains(und.TrimStart('*')))
23 | {
24 | // vsfilter ingore starting asterisk
25 | logger?.ZLogWarning($"Style '{und}' should remove the starting asterisk");
26 | continue;
27 | }
28 |
29 | if (ass.Events.Collection.Where(x => x.Style == und).All(x => string.IsNullOrEmpty(x.Text)))
30 | {
31 | logger?.ZLogWarning($"Please check style '{und}', it may have been actually used but not defined");
32 | continue;
33 | }
34 |
35 | undefinedStyles.Add(und);
36 | }
37 |
38 | if (undefinedStyles.Count > 0)
39 | {
40 | throw new Exception($"Undefined styles in ass Styles section: {string.Join(", ", undefinedStyles)}");
41 | }
42 |
43 | return anlz.GetUsedFontInfos();
44 | }
45 |
46 | public static bool IsMatch(AssFontInfo afi, FontInfo fi, bool single, int? minimalWeight = null, bool? hadItalic = null, ILogger? logger = null)
47 | {
48 | var boldMatch = false;
49 | var italicMatch = false;
50 | if (!single) { if (minimalWeight is null || hadItalic is null) throw new ArgumentNullException(); }
51 |
52 | logger?.ZLogDebug($"Try match {afi.ToString()} and {string.Join('|', fi.FamilyNames.Values.Distinct())}_w{fi.Weight}_b{(fi.Bold ? 1 : 0)}_i{(fi.Italic ? 1 : 0)}");
53 | switch (afi.Weight)
54 | {
55 | case 0:
56 | boldMatch = fi.Bold ? single : true; // cant get only true bold
57 | break;
58 | case 1:
59 | if (single)
60 | {
61 | // The following cases exist:
62 | // 1. Only the bold weight font file has been correctly matched
63 | // 2. Font weight less than 550, get faux bold
64 | // 3. Font weight great than or equal 550, \b1 is invalid
65 | if (fi.Weight >= 550) { logger?.ZLogWarning($"{afi.Name} use \\b1 will not get faux bold"); }
66 | boldMatch = true;
67 | }
68 | else
69 | {
70 | // strict
71 | boldMatch = fi.Bold;
72 | }
73 | break;
74 | default:
75 | if (afi.Weight == fi.Weight)
76 | {
77 | boldMatch = true;
78 | }
79 | else
80 | {
81 | if (fi.Weight > (afi.Weight + 150)) { logger?.ZLogDebug($"{afi.Name} should use \\b{fi.Weight}"); }
82 | }
83 | break;
84 | }
85 |
86 | if (afi.Italic)
87 | {
88 | if (fi.Italic)
89 | {
90 | italicMatch = true;
91 | }
92 | else
93 | {
94 | // maybe faux italic
95 | if (single) { italicMatch = true; }
96 | else
97 | {
98 | if (!(bool)hadItalic!) { italicMatch = true; }
99 | else if (!(fi.MaxpNumGlyphs < 6000 && !fi.FamilyNames.Keys.Any(key => key is 2052 or 1028 or 3076 or 5124)))
100 | {
101 | // maybe cjk fonts
102 | italicMatch = true;
103 | logger?.ZLogDebug($"{afi.Name} use \\i1 maybe get faux italic");
104 | }
105 | }
106 | }
107 | }
108 | else
109 | {
110 | if (!fi.Italic) { italicMatch = true; }
111 | }
112 |
113 | return boldMatch && italicMatch;
114 | }
115 |
116 | public static FontInfo? GetMatchedFontInfo(AssFontInfo afi, IGrouping fig, ILogger? logger = null)
117 | {
118 | var assFn = afi.Name.StartsWith('@') ? afi.Name.AsSpan(1) : afi.Name.AsSpan();
119 | if (!(assFn.SequenceEqual(fig.Key.AsSpan()) || fig.First().FamilyNames.ContainsValue(assFn.ToString()))) { return null; }
120 |
121 | if (fig.Count() == 1)
122 | {
123 | if (IsMatch(afi, fig.First(), true, null, null, logger)) { return fig.First(); }
124 | else { return null; }
125 | }
126 | else
127 | {
128 | var minimalWeight = fig.Select(fi => fi.Weight).Min();
129 | var hadItalic = fig.Select(fi => fi.Italic is true).Any();
130 | foreach (var fi in fig)
131 | {
132 | if (IsMatch(afi, fi, false, minimalWeight, hadItalic, logger)) { return fi; }
133 | }
134 | return null;
135 | }
136 | }
137 |
138 | }
139 |
--------------------------------------------------------------------------------
/AssFontSubset.Core/src/FontConstant.cs:
--------------------------------------------------------------------------------
1 | namespace AssFontSubset.Core;
2 |
3 | public static class FontConstant
4 | {
5 | // Unicode 15.1
6 | public static Dictionary VertMapping = new Dictionary()
7 | {
8 | // Vertical Forms
9 | { '\u002c', '\ufe10' },
10 | { '\u3001', '\ufe11' },
11 | { '\u3002', '\ufe12' },
12 | { '\u003a', '\ufe13' },
13 | { '\u003b', '\ufe14' },
14 | { '\u0021', '\ufe15' },
15 | { '\u003f', '\ufe16' },
16 | { '\u3016', '\ufe17' },
17 | { '\u3017', '\ufe18' },
18 | { '\u2026', '\ufe19' },
19 |
20 | // CJK Compatibility Forms - Glyphs for vertical variants
21 | // { '', '\ufe30' },
22 | { '\u2014', '\ufe31' },
23 | { '\u2013', '\ufe32' },
24 | // { '\u005f', '\ufe33' },
25 | // { '', '\ufe34' },
26 | { '\u0028', '\ufe35' },
27 | { '\u0029', '\ufe36' },
28 | { '\u007b', '\ufe37' },
29 | { '\u007d', '\ufe38' },
30 | { '\u3014', '\ufe39' },
31 | { '\u3015', '\ufe3a' },
32 | { '\u3010', '\ufe3b' },
33 | { '\u3011', '\ufe3c' },
34 | { '\u300a', '\ufe3d' },
35 | { '\u300b', '\ufe3e' },
36 | { '\u2329', '\ufe3f' },
37 | { '\u232a', '\ufe40' },
38 | { '\u300c', '\ufe41' },
39 | { '\u300d', '\ufe42' },
40 | { '\u300e', '\ufe43' },
41 | { '\u300f', '\ufe44' },
42 | { '\u005b', '\ufe47' },
43 | { '\u005d', '\ufe48' },
44 | };
45 |
46 | public const int LanguageIdEnUs = 1033;
47 |
48 |
49 | // GDI doesn’t seem to use any features (may use vert?), and it has its own logic for handling vertical layout.
50 | // https://learn.microsoft.com/en-us/typography/opentype/spec/features_uz#tag-vrt2
51 | // GDI may according it:
52 | // OpenType font with CFF outlines to be used for vertical writing must have vrt2, otherwise fallback
53 | // OpenType font without CFF outlines use vert map default glyphs to vertical writing glyphs
54 |
55 | // https://github.com/libass/libass/pull/702
56 | // libass seems to be trying to use features like vert to solve this problem.
57 | // These are features related to vertical layout but are not enabled: "vchw", "vhal", "vkrn", "vpal", "vrtr".
58 | // https://github.com/libass/libass/blob/6e83137cdbaf4006439d526fef902e123129707b/libass/ass_shaper.c#L147
59 | public static readonly string[] SubsetKeepFeatures = [
60 | "vert", "vrtr",
61 | "vrt2",
62 | "vkna",
63 | ];
64 | }
--------------------------------------------------------------------------------
/AssFontSubset.Core/src/FontParse.cs:
--------------------------------------------------------------------------------
1 | using Mobsub.Helper.Font;
2 |
3 | namespace AssFontSubset.Core;
4 |
5 | public struct FontInfo
6 | {
7 | public Dictionary FamilyNames;
8 | //public bool Regular;
9 | public bool Bold;
10 | public bool Italic;
11 | public int Weight;
12 | //public bool MaybeHasTrueBoldOrItalic;
13 | public string FileName;
14 | public uint Index;
15 | public ushort MaxpNumGlyphs;
16 |
17 | public override bool Equals(object? obj)
18 | {
19 | return obj is FontInfo info &&
20 | FamilyNames == info.FamilyNames &&
21 | //Regular == info.Regular &&
22 | Bold == info.Bold &&
23 | Italic == info.Italic &&
24 | Weight == info.Weight &&
25 | //MaybeHasTrueBoldOrItalic == info.MaybeHasTrueBoldOrItalic &&
26 | FileName == info.FileName &&
27 | Index == info.Index &&
28 | MaxpNumGlyphs == info.MaxpNumGlyphs;
29 | }
30 |
31 | public override int GetHashCode()
32 | {
33 | HashCode hash = new HashCode();
34 | hash.Add(FamilyNames);
35 | //hash.Add(Regular);
36 | hash.Add(Bold);
37 | hash.Add(Italic);
38 | hash.Add(Weight);
39 | //hash.Add(MaybeHasTrueBoldOrItalic);
40 | hash.Add(FileName);
41 | hash.Add(Index);
42 | hash.Add(MaxpNumGlyphs);
43 | return hash.ToHashCode();
44 | }
45 |
46 | public static bool operator ==(FontInfo lhs, FontInfo rhs) => lhs.Equals(rhs);
47 | public static bool operator !=(FontInfo lhs, FontInfo rhs) => !lhs.Equals(rhs);
48 | }
49 |
50 | public static class FontParse
51 | {
52 | public static List GetFontInfos(DirectoryInfo dirInfo)
53 | {
54 | List fontInfos = [];
55 | var fileInfos = dirInfo.GetFiles();
56 | var faceInfos = OpenType.GetLocalFontsInfo(fileInfos);
57 |
58 | foreach (var faceInfo in faceInfos)
59 | {
60 | fontInfos.Add(ConvertToFontInfo(faceInfo));
61 | }
62 |
63 | return fontInfos;
64 | }
65 |
66 | private static FontInfo ConvertToFontInfo(FontFaceInfoBase faceInfo)
67 | {
68 | var info = (FontFaceInfoOpenType)faceInfo;
69 | var fsSel = info.fsSelection;
70 | var familyNamesNew = info.FamilyNamesGdi!;
71 |
72 | if (!familyNamesNew.ContainsKey(FontConstant.LanguageIdEnUs))
73 | {
74 | familyNamesNew.Add(FontConstant.LanguageIdEnUs, familyNamesNew.FirstOrDefault().Value);
75 | }
76 |
77 | return new FontInfo
78 | {
79 | FamilyNames = familyNamesNew,
80 | //Regular = ((fsSel & 0b_0100_0000) >> 6) == 1, // bit 6
81 | Bold = ((fsSel & 0b_0010_0000) >> 5) == 1, // bit 5
82 | Italic = (fsSel & 0b_1) == 1, // bit 0
83 | Weight = info.Weight,
84 | //MaybeHasTrueBoldOrItalic = false,
85 | FileName = info.FileInfo!.FilePath!,
86 | Index = info.FaceIndex,
87 | MaxpNumGlyphs = info.MaxpNumGlyphs,
88 | };
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/AssFontSubset.Core/src/HarfBuzzSubset.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using System.Text;
3 | using HarfBuzzBinding;
4 | using Microsoft.Extensions.Logging;
5 | using ZLogger;
6 | using SubsetApis = HarfBuzzBinding.Native.Subset.Apis;
7 | using HBApis = HarfBuzzBinding.Native.Apis;
8 | using System.Diagnostics;
9 | using HarfBuzzBinding.Native.Subset;
10 | using OTFontFile;
11 |
12 | namespace AssFontSubset.Core;
13 |
14 | public unsafe class HarfBuzzSubset(ILogger? logger) : SubsetToolBase
15 | {
16 | private Version hbssVersion = new Version(Methods.GetHarfBuzzVersion()!);
17 | public SubsetConfig Config;
18 | public Stopwatch? sw;
19 | private long timer;
20 |
21 | public override void SubsetFonts(Dictionary> subsetFonts, string outputFolder, out Dictionary nameMap)
22 | {
23 | logger?.ZLogInformation($"Start subset font");
24 | logger?.ZLogInformation($"Font subset use harfbuzz-subset {hbssVersion}");
25 | nameMap = [];
26 | logger?.ZLogDebug($"Generate randomly non repeating font names");
27 | var randoms = SubsetFont.GenerateRandomStrings(8, subsetFonts.Keys.Count);
28 |
29 | var i = 0;
30 | foreach (var kv in subsetFonts)
31 | {
32 | nameMap[kv.Key] = randoms[i];
33 | foreach (var subsetFont in kv.Value)
34 | {
35 | subsetFont.RandomNewName = randoms[i];
36 | logger?.ZLogInformation($"Start subset {subsetFont.OriginalFontFile.Name}");
37 | timer = 0;
38 | CreateFontSubset(subsetFont, outputFolder);
39 | logger?.ZLogInformation($"Subset font completed, use {timer} ms");
40 | }
41 |
42 | i++;
43 | }
44 | }
45 |
46 | public override void CreateFontSubset(SubsetFont ssf, string outputFolder)
47 | {
48 | if (!Path.Exists(outputFolder))
49 | {
50 | new DirectoryInfo(outputFolder).Create();
51 | }
52 |
53 | var outputFileWithoutSuffix = Path.GetFileNameWithoutExtension(ssf.OriginalFontFile.Name);
54 | var outputFile = new StringBuilder($"{outputFileWithoutSuffix}.{ssf.TrackIndex}.{ssf.RandomNewName}");
55 |
56 | var originalFontFileSuffix = Path.GetExtension(ssf.OriginalFontFile.Name).AsSpan();
57 | var outFileWithoutSuffix = outputFile.ToString();
58 | outputFile.Append(originalFontFileSuffix[..3]);
59 | switch (originalFontFileSuffix[^1])
60 | {
61 | case 'c':
62 | outputFile.Append('f');
63 | break;
64 | case 'C':
65 | outputFile.Append('F');
66 | break;
67 | default:
68 | outputFile.Append(originalFontFileSuffix[^1]);
69 | break;
70 | }
71 |
72 | var outputFileName = Path.Combine(outputFolder, outputFile.ToString());
73 |
74 | ssf.Preprocessing();
75 | var modifyIds = GetModifyNameIds(ssf.OriginalFontFile.FullName, ssf.TrackIndex);
76 | if (Config.DebugMode)
77 | {
78 | ssf.CharactersFile = Path.Combine(outputFolder, $"{outFileWithoutSuffix}.txt");
79 | ssf.WriteRunesToUtf8File();
80 | }
81 |
82 | sw ??= new Stopwatch();
83 | sw.Start();
84 |
85 | _ = Methods.TryGetFontFace(ssf.OriginalFontFile.FullName, ssf.TrackIndex, out var facePtr);
86 | //facePtr = SubsetApis.hb_subset_preprocess(facePtr);
87 |
88 | var input = SubsetApis.hb_subset_input_create_or_fail();
89 |
90 | var unicodes = SubsetApis.hb_subset_input_unicode_set(input);
91 | foreach (var rune in ssf.Runes)
92 | {
93 | HBApis.hb_set_add(unicodes, (uint)rune.Value);
94 | }
95 |
96 | var features = SubsetApis.hb_subset_input_set(input, hb_subset_sets_t.HB_SUBSET_SETS_LAYOUT_FEATURE_TAG);
97 | HBApis.hb_set_clear(features);
98 | foreach (var feature in FontConstant.SubsetKeepFeatures)
99 | {
100 | HBApis.hb_set_add(features, HBApis.hb_tag_from_string((sbyte*)Marshal.StringToHGlobalAnsi(feature), -1));
101 | }
102 |
103 | // need drop hinting?
104 |
105 | Methods.RenameFontname(input,
106 | (sbyte*)Marshal.StringToHGlobalAnsi($"Processed by AssFontSubset v{System.Reflection.Assembly.GetEntryAssembly()!.GetName().Version}; harfbuzz-subset {hbssVersion}"),
107 | (sbyte*)Marshal.StringToHGlobalAnsi(ssf.RandomNewName),
108 | modifyIds);
109 |
110 | var faceNewPtr = SubsetApis.hb_subset_or_fail(facePtr, input);
111 |
112 | var blobPtr = HBApis.hb_face_reference_blob(faceNewPtr);
113 | Methods.WriteFontFile(blobPtr, outputFileName);
114 |
115 | sw.Stop();
116 | timer += sw.ElapsedMilliseconds;
117 | sw.Reset();
118 |
119 | SubsetApis.hb_subset_input_destroy(input);
120 | HBApis.hb_face_destroy(faceNewPtr);
121 | HBApis.hb_face_destroy(facePtr);
122 | }
123 |
124 | private static OpenTypeNameId[] GetModifyNameIds(string fontFileName, uint index)
125 | {
126 | List ids = [];
127 | var otf = new OTFile();
128 | otf.open(fontFileName);
129 | var font = otf.GetFont(index);
130 | var nameTable = (Table_name)font!.GetTable("name")!;
131 | for (uint i = 0; i < nameTable.NumberNameRecords; i++)
132 | {
133 | var nameRecord = nameTable.GetNameRecord(i);
134 | if (nameRecord!.NameID is 0 or 1 or 3 or 4 or 6)
135 | {
136 | ids.Add(new OpenTypeNameId
137 | {
138 | NameId = nameRecord.NameID,
139 | PlatformId = nameRecord.PlatformID,
140 | LanguageId = nameRecord.LanguageID,
141 | EncodingId = nameRecord.EncodingID,
142 | });
143 | }
144 | }
145 |
146 | return ids.ToArray();
147 | }
148 | }
--------------------------------------------------------------------------------
/AssFontSubset.Core/src/PyFontTools.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using System.Diagnostics;
3 | using System.Text;
4 | using System.Xml;
5 | using ZLogger;
6 |
7 | namespace AssFontSubset.Core;
8 |
9 | public class PyFontTools(string pyftsubset, string ttx, ILogger? logger) : SubsetToolBase
10 | {
11 | private Version pyFtVersion = GetFontToolsVersion(ttx);
12 |
13 | public SubsetConfig Config;
14 | public Stopwatch? sw;
15 | private long timer;
16 |
17 | public void SubsetFonts(List subsetFonts, string outputFolder)
18 | {
19 | logger?.ZLogInformation($"Font subset use pyFontTools {pyFtVersion}");
20 | var randoms = SubsetFont.GenerateRandomStrings(8, subsetFonts.Count);
21 | var num = 0;
22 | foreach (var subsetFont in subsetFonts)
23 | {
24 | subsetFont.RandomNewName = randoms[num];
25 | CreateFontSubset(subsetFont, outputFolder);
26 | DumpFont(subsetFont);
27 | ChangeXmlFontName(subsetFont);
28 | CompileFont(subsetFont);
29 |
30 | if (!Config.DebugMode)
31 | {
32 | DeleteTempFiles(subsetFont);
33 | }
34 |
35 | num++;
36 | }
37 | }
38 |
39 | public override void SubsetFonts(Dictionary> subsetFonts, string outputFolder, out Dictionary nameMap)
40 | {
41 | logger?.ZLogInformation($"Start subset font");
42 | logger?.ZLogInformation($"Font subset use pyFontTools {pyFtVersion}");
43 | nameMap = [];
44 | logger?.ZLogDebug($"Generate randomly non repeating font names");
45 | var randoms = SubsetFont.GenerateRandomStrings(8, subsetFonts.Keys.Count);
46 |
47 | var i = 0;
48 | foreach (var kv in subsetFonts)
49 | {
50 | nameMap[kv.Key] = randoms[i];
51 | foreach (var subsetFont in kv.Value)
52 | {
53 | subsetFont.RandomNewName = randoms[i];
54 | logger?.ZLogInformation($"Start subset {subsetFont.OriginalFontFile.Name}");
55 | timer = 0;
56 | CreateFontSubset(subsetFont, outputFolder);
57 | DumpFont(subsetFont);
58 | ChangeXmlFontName(subsetFont);
59 | CompileFont(subsetFont);
60 | logger?.ZLogInformation($"Subset font completed, use {timer} ms");
61 |
62 | if (!Config.DebugMode)
63 | {
64 | DeleteTempFiles(subsetFont);
65 | }
66 | }
67 | i++;
68 | }
69 | }
70 |
71 | public override void CreateFontSubset(SubsetFont ssf, string outputFolder)
72 | {
73 | if (!Path.Exists(outputFolder))
74 | {
75 | new DirectoryInfo(outputFolder).Create();
76 | }
77 |
78 | var outputFileWithoutSuffix = Path.GetFileNameWithoutExtension(ssf.OriginalFontFile.Name);
79 | var outputFileMain = $"{outputFileWithoutSuffix}.{ssf.TrackIndex}.{ssf.RandomNewName}";
80 |
81 | ssf.CharactersFile = Path.Combine(outputFolder, $"{outputFileMain}.txt");
82 | ssf.SubsetFontFileTemp = Path.Combine(outputFolder, $"{outputFileMain}._tmp_");
83 | ssf.SubsetFontTtxTemp = Path.Combine(outputFolder, $"{outputFileMain}.ttx");
84 |
85 | ssf.Preprocessing();
86 | ssf.WriteRunesToUtf8File();
87 |
88 | var subsetCmd = GetSubsetCmd(ssf);
89 | ExecuteCmd(subsetCmd);
90 | }
91 |
92 | public void DumpFont(SubsetFont ssf) => ExecuteCmd(GetDumpFontCmd(ssf));
93 |
94 | private void CompileFont(SubsetFont ssf) => ExecuteCmd(GetCompileFontCmd(ssf));
95 |
96 | private void DeleteTempFiles(SubsetFont ssf)
97 | {
98 | logger?.ZLogDebug($"Start delete temp files:{Environment.NewLine}temp subset font files:{ssf.SubsetFontFileTemp}{Environment.NewLine}ttx files:{ssf.SubsetFontTtxTemp}{Environment.NewLine}glyphs files:{ssf.CharactersFile}");
99 | File.Delete(ssf.SubsetFontFileTemp!);
100 | File.Delete(ssf.SubsetFontTtxTemp!);
101 | File.Delete(ssf.CharactersFile!);
102 | logger?.ZLogDebug($"Clean completed");
103 | }
104 |
105 | private void ChangeXmlFontName(SubsetFont font)
106 | {
107 | var ttxFile = font.SubsetFontTtxTemp;
108 |
109 | if (!File.Exists(ttxFile))
110 | {
111 | throw new Exception($"Font dump to ttx failed, please use FontForge remux font. {Environment.NewLine}" +
112 | $"File: {font.OriginalFontFile}");
113 | }
114 |
115 | var ttxContent = File.ReadAllText(ttxFile);
116 | ttxContent = ttxContent.Replace("\0", ""); // remove null characters. it might be a bug in ttx.exe.
117 | var replaced = false;
118 |
119 | var specialFont = ""; // special hack for some fonts
120 |
121 | var xd = new XmlDocument();
122 | xd.LoadXml(ttxContent);
123 |
124 | // replace font name
125 | var namerecords = xd.SelectNodes(@"ttFont/name/namerecord");
126 |
127 | foreach (XmlNode record in namerecords!)
128 | {
129 | string nameID = record.Attributes!["nameID"]!.Value.Trim();
130 | switch (nameID)
131 | {
132 | case "0":
133 | record.InnerText = $"Processed by AssFontSubset v{System.Reflection.Assembly.GetEntryAssembly()!.GetName().Version}; pyFontTools {pyFtVersion}";
134 | break;
135 | case "1":
136 | case "3":
137 | case "4":
138 | case "6":
139 | if (record.InnerText.Contains("Source Han"))
140 | {
141 | specialFont = "Source Han";
142 | }
143 | record.InnerText = font.RandomNewName!;
144 | replaced = true;
145 | break;
146 | default:
147 | break;
148 | }
149 | }
150 |
151 | // remove substitution for ellipsis for source han sans/serif font
152 | if (Config.SourceHanEllipsis && specialFont == "Source Han")
153 | {
154 | SourceHanFontEllipsis(ref xd);
155 | }
156 |
157 | xd.Save(ttxFile);
158 |
159 | if (!replaced)
160 | {
161 | throw new Exception($"Font name replacement failed, please use FontForge remux font. {Environment.NewLine}" +
162 | $"File: {font.OriginalFontFile}");
163 | }
164 | }
165 |
166 | // Special Hack for Source Han Sans & Source Han Serif ellipsis
167 | private static void SourceHanFontEllipsis(ref XmlDocument xd)
168 | {
169 | // find cid for ellipsis (\u2026)
170 | var cmap = xd.SelectSingleNode(@"//map[@code='0x2026']");
171 | if (cmap != null)
172 | {
173 | var ellipsisCid = cmap.Attributes!["name"]!.Value; // why Trim()
174 | XmlNodeList substitutionNodes = xd.SelectNodes($"//Substitution[@in='{ellipsisCid}']")!;
175 | // remove substitution for lower ellipsis.
176 | // NOTE: Vertical ellipsis is cid5xxxxx, and we need to keep it. Hopefully Adobe won't change it.
177 | foreach (XmlNode sNode in substitutionNodes)
178 | {
179 | if (sNode.Attributes!["out"]!.Value.StartsWith("cid6"))
180 | {
181 | sNode.ParentNode!.RemoveChild(sNode);
182 | }
183 | }
184 | }
185 | }
186 |
187 |
188 | private void ExecuteCmd(ProcessStartInfo startInfo)
189 | {
190 | sw ??= new Stopwatch();
191 | var success = true;
192 | sw.Start();
193 | using var process = Process.Start(startInfo);
194 | logger?.ZLogDebug($"Start command: {startInfo.FileName} {string.Join(' ', startInfo.ArgumentList)}");
195 |
196 | if (process != null)
197 | {
198 | var output = process.StandardOutput;
199 | var errorOutput = process.StandardError.ReadToEnd();
200 | var outputStr = output.ReadToEnd();
201 |
202 | logger?.ZLogDebug($"Executing...");
203 | process.WaitForExit();
204 | var exitCode = process.ExitCode;
205 |
206 | sw.Stop();
207 |
208 | if (exitCode != 0)
209 | {
210 | logger?.ZLogError($"Return exitcode {exitCode}, error output: {errorOutput.TrimEnd()}");
211 | success = false;
212 | }
213 | else
214 | {
215 | logger?.ZLogDebug($"Output:{Environment.NewLine}{errorOutput.TrimEnd()}");
216 | logger?.ZLogDebug($"Successfully executed, use {sw.ElapsedMilliseconds} ms");
217 | }
218 | timer += sw.ElapsedMilliseconds;
219 | }
220 | else
221 | {
222 | success = false;
223 | logger?.ZLogDebug($"Process not start");
224 | }
225 |
226 | sw.Reset();
227 | if (!success)
228 | {
229 | throw new Exception($"Command execution failed: {startInfo.FileName} {string.Join(' ', startInfo.ArgumentList)}");
230 | }
231 |
232 | //return success;
233 | }
234 |
235 | private static ProcessStartInfo GetSimpleCmd(string exe) => new()
236 | {
237 | FileName = exe,
238 | UseShellExecute = false,
239 | RedirectStandardOutput = true,
240 | RedirectStandardInput = true,
241 | RedirectStandardError = true,
242 | CreateNoWindow = true,
243 | StandardErrorEncoding = Encoding.UTF8,
244 | StandardOutputEncoding = Encoding.UTF8,
245 | };
246 |
247 | private ProcessStartInfo GetSubsetCmd(SubsetFont ssf)
248 | {
249 | var startInfo = GetSimpleCmd(pyftsubset);
250 |
251 | string[] argus = [
252 | ssf.OriginalFontFile.FullName,
253 | $"--text-file={ssf.CharactersFile!}",
254 | $"--output-file={ssf.SubsetFontFileTemp!}",
255 | "--name-languages=*",
256 | $"--font-number={ssf.TrackIndex}",
257 | // "--no-layout-closure",
258 | $"--layout-features={string.Join(",", FontConstant.SubsetKeepFeatures)}",
259 | // "--layout-features=*",
260 | ];
261 | foreach (var arg in argus)
262 | {
263 | startInfo.ArgumentList.Add(arg);
264 | }
265 |
266 | if (pyFtVersion > new Version("4.44.0"))
267 | {
268 | // https://github.com/fonttools/fonttools/releases/tag/4.44.1
269 | // Affects VSFilter vertical layout, it can’t find correct fonts when change OS/2 ulCodePageRange*
270 | // Perhaps it only works with OpenType font that don’t have CFF outlines
271 | startInfo.ArgumentList.Add("--no-prune-codepage-ranges");
272 | }
273 |
274 | startInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
275 | return startInfo;
276 | }
277 |
278 | private ProcessStartInfo GetDumpFontCmd(SubsetFont ssf)
279 | {
280 | var startInfo = GetSimpleCmd(ttx);
281 | startInfo.ArgumentList.Add("-f");
282 | startInfo.ArgumentList.Add("-o");
283 | startInfo.ArgumentList.Add(ssf.SubsetFontTtxTemp!);
284 | startInfo.ArgumentList.Add(ssf.SubsetFontFileTemp!);
285 | //startInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
286 | return startInfo;
287 | }
288 |
289 | private ProcessStartInfo GetCompileFontCmd(SubsetFont ssf)
290 | {
291 | var startInfo = GetSimpleCmd(ttx);
292 | startInfo.ArgumentList.Add("-f");
293 |
294 | // https://github.com/libass/libass/issues/619#issuecomment-1244561188
295 | // Don’t recalc glyph bounding boxes
296 | startInfo.ArgumentList.Add("-b");
297 |
298 | startInfo.ArgumentList.Add(ssf.SubsetFontTtxTemp!);
299 | startInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
300 | return startInfo;
301 | }
302 |
303 | private static Version GetFontToolsVersion(string ttxPath)
304 | {
305 | Version version;
306 | var startInfo = GetSimpleCmd(ttxPath);
307 | startInfo.ArgumentList.Add("--version");
308 | using var process = Process.Start(startInfo);
309 | if (process != null)
310 | {
311 | version = new Version(process.StandardOutput.ReadToEnd().Trim('\n'));
312 | process.WaitForExit();
313 | }
314 | else
315 | {
316 | throw new Exception($"Command execution failed: {startInfo.FileName} {string.Join(' ', startInfo.ArgumentList)}");
317 | }
318 |
319 | return version;
320 | }
321 | }
322 |
--------------------------------------------------------------------------------
/AssFontSubset.Core/src/SubsetConfig.cs:
--------------------------------------------------------------------------------
1 | namespace AssFontSubset.Core;
2 |
3 | public struct SubsetConfig
4 | {
5 | public bool SourceHanEllipsis;
6 | public bool DebugMode;
7 | public SubsetBackend Backend;
8 | }
9 |
10 | public enum SubsetBackend
11 | {
12 | PyFontTools = 1,
13 | HarfBuzzSubset = 2,
14 | }
--------------------------------------------------------------------------------
/AssFontSubset.Core/src/SubsetCore.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using Mobsub.SubtitleParse.AssTypes;
3 | using Mobsub.SubtitleParse;
4 | using System.Text;
5 | using Microsoft.Extensions.Logging;
6 | using ZLogger;
7 | using System.Diagnostics;
8 |
9 | namespace AssFontSubset.Core;
10 |
11 | public class SubsetCore(ILogger? logger = null)
12 | {
13 | private static readonly Stopwatch _stopwatch = new();
14 |
15 | public async Task SubsetAsync(FileInfo[] path, DirectoryInfo? fontPath, DirectoryInfo? outputPath, DirectoryInfo? binPath, SubsetConfig subsetConfig)
16 | {
17 | var baseDir = path[0].Directory!.FullName;
18 | fontPath ??= new DirectoryInfo(Path.Combine(baseDir, "fonts"));
19 | outputPath ??= new DirectoryInfo(Path.Combine(baseDir, "output"));
20 |
21 | foreach (var file in path)
22 | {
23 | if (!file.Exists)
24 | {
25 | throw new Exception($"Please check if file {file} exists");
26 | }
27 | }
28 | if (!fontPath.Exists) { throw new Exception($"Please check if directory {fontPath} exists"); }
29 | if (outputPath.Exists) { outputPath.Delete(true); }
30 | var fontDir = fontPath.FullName;
31 | var optDir = outputPath.FullName;
32 |
33 | await Task.Run(() =>
34 | {
35 | var fontInfos = GetFontInfoFromFiles(fontDir);
36 | var assFonts = GetAssFontInfoFromFiles(path, optDir, out var assMulti);
37 | var subsetFonts = GetSubsetFonts(fontInfos, assFonts, out var fontMap);
38 | Dictionary nameMap = [];
39 |
40 | switch (subsetConfig.Backend)
41 | {
42 | case SubsetBackend.PyFontTools:
43 | var pyftsubset = binPath is null ? "pyftsubset" : Path.Combine(binPath.FullName, "pyftsubset");
44 | var ttx = binPath is null ? "ttx" : Path.Combine(binPath.FullName, "ttx");
45 | var pyFT = new PyFontTools(pyftsubset, ttx, logger) { Config = subsetConfig, sw = _stopwatch };
46 | pyFT.SubsetFonts(subsetFonts, optDir, out nameMap);
47 | break;
48 | case SubsetBackend.HarfBuzzSubset:
49 | var hbss = new HarfBuzzSubset(logger) { Config = subsetConfig, sw = _stopwatch };
50 | hbss.SubsetFonts(subsetFonts, optDir, out nameMap);
51 | break;
52 | default:
53 | throw new ArgumentOutOfRangeException();
54 | }
55 |
56 | foreach (var kv in assMulti)
57 | {
58 | ChangeAssFontName(kv.Value, nameMap, fontMap);
59 | kv.Value.WriteAssFile(kv.Key);
60 | }
61 | });
62 | }
63 |
64 | private IEnumerable> GetFontInfoFromFiles(string dir)
65 | {
66 | string[] supportFonts = [".ttf", ".otf", ".ttc", "otc"];
67 | // HashSet HasTrueBoldOrItalicRecord = [];
68 |
69 | logger?.ZLogInformation($"Start scan valid font files in {dir}");
70 | logger?.ZLogInformation($"Support font file extension: {string.Join(", ", supportFonts)}");
71 | _stopwatch.Start();
72 |
73 | var dirInfo = new DirectoryInfo(dir);
74 | var fontInfos = FontParse.GetFontInfos(dirInfo);
75 |
76 | _stopwatch.Stop();
77 | logger?.ZLogDebug($"Font file scanning completed, use {_stopwatch.ElapsedMilliseconds} ms");
78 | _stopwatch.Reset();
79 |
80 | if (TryCheckDuplicatFonts(fontInfos, out var fontInfoGroup))
81 | {
82 | throw new Exception($"Maybe have duplicate fonts in fonts directory");
83 | }
84 |
85 | return fontInfoGroup;
86 | }
87 |
88 | private bool TryCheckDuplicatFonts(List fontInfos, out IEnumerable> fontInfoGroup)
89 | {
90 | var dupFonts = false;
91 | fontInfoGroup = fontInfos.GroupBy(fontInfo => fontInfo.FamilyNames[FontConstant.LanguageIdEnUs]);
92 | foreach (var group in fontInfoGroup)
93 | {
94 | if (group.Count() <= 1) continue;
95 | var groupWithoutFileNames = group.GroupBy(fi => new
96 | {
97 | fi.Bold,
98 | fi.Italic,
99 | fi.Weight,
100 | fi.Index,
101 | fi.MaxpNumGlyphs,
102 | });
103 |
104 | foreach (var g in groupWithoutFileNames)
105 | {
106 | if (g.Count() <= 1) continue;
107 | logger?.ZLogError($"Duplicate fonts: {string.Join('、', g.Select(x => x.FileName))}");
108 | dupFonts = true;
109 | }
110 | }
111 |
112 | return dupFonts;
113 | }
114 |
115 | private Dictionary> GetAssFontInfoFromFiles(FileInfo[] assFiles, string optDir, out Dictionary assDataWithOutputName)
116 | {
117 | assDataWithOutputName = [];
118 | Dictionary> multiAssFonts = [];
119 |
120 | logger?.ZLogInformation($"Start parse font info from ass files");
121 | _stopwatch.Start();
122 |
123 | foreach (var assFile in assFiles)
124 | {
125 | //logger?.ZLogInformation($"{assFile.FullName}");
126 | var assFileNew = Path.Combine(optDir, assFile.Name);
127 | var assFonts = AssFont.GetAssFonts(assFile.FullName, out var ass, logger);
128 |
129 | foreach (var kv in assFonts)
130 | {
131 | if (multiAssFonts.Count > 0 && multiAssFonts.TryGetValue(kv.Key, out var value))
132 | {
133 | value.UnionWith(kv.Value);
134 | }
135 | else
136 | {
137 | multiAssFonts.Add(kv.Key, kv.Value);
138 | }
139 | }
140 | assDataWithOutputName.Add(assFileNew, ass);
141 | }
142 |
143 | _stopwatch.Stop();
144 | logger?.ZLogInformation($"Ass font info parsing completed, use {_stopwatch.ElapsedMilliseconds} ms");
145 | _stopwatch.Reset();
146 | return multiAssFonts;
147 | }
148 |
149 | Dictionary> GetSubsetFonts(IEnumerable> fontInfos, Dictionary> assFonts, out Dictionary> fontMap)
150 | {
151 | logger?.ZLogInformation($"Start generate subset font info");
152 | _stopwatch.Start();
153 |
154 | logger?.ZLogDebug($"Start match font file info and ass font info");
155 | fontMap = [];
156 | List matchedAssFontInfos = [];
157 |
158 | // var fiGroups = fontInfos.GroupBy(fontInfo => fontInfo.FamilyNames[FontConstant.LanguageIdEnUs]);
159 | foreach (var fig in fontInfos)
160 | {
161 | foreach (var afi in assFonts.Keys)
162 | {
163 | if (matchedAssFontInfos.Contains(afi)) { continue; }
164 | var _fontInfo = AssFont.GetMatchedFontInfo(afi, fig, logger);
165 | if (_fontInfo == null) { continue; }
166 | var fontInfo = (FontInfo) _fontInfo;
167 |
168 | if (!fontMap.TryGetValue(fontInfo, out var _))
169 | {
170 | fontMap.Add(fontInfo, []);
171 | }
172 | fontMap[fontInfo].Add(afi);
173 |
174 | matchedAssFontInfos.Add(afi);
175 | logger?.ZLogDebug($"{afi.ToString()} match {fontInfo.FileName} index {fontInfo.Index}");
176 | }
177 | }
178 | logger?.ZLogDebug($"Match completed");
179 |
180 | if (matchedAssFontInfos.Count != assFonts.Keys.Count)
181 | {
182 | var NotFound = assFonts.Keys.Except(matchedAssFontInfos).ToList();
183 | throw new Exception($"Not found font file: {string.Join("、", NotFound.Select(x => x.ToString()))}");
184 | }
185 |
186 | logger?.ZLogDebug($"Start convert font file info to subset font info");
187 | //List subsetFonts = [];
188 | Dictionary> subsetFonts = [];
189 | foreach (var kv in fontMap)
190 | {
191 | // var runes = kv.Value.Count > 1 ? kv.Value.SelectMany(i => assFonts[i]).ToHashSet().ToList() : assFonts[kv.Value[0]];
192 | HashSet horRunes = [];
193 | HashSet vertRunes = [];
194 | foreach (var afi in kv.Value)
195 | {
196 | if (afi.Name.StartsWith('@'))
197 | {
198 | vertRunes.UnionWith(assFonts[afi]);
199 | }
200 | else
201 | {
202 | horRunes.UnionWith(assFonts[afi]);
203 | }
204 | }
205 |
206 | //subsetFonts.Add(new SubsetFont(new FileInfo(kv.Key.FileName), kv.Key.Index, runes) { OriginalFamilyName = kv.Key.FamilyName });
207 | var _famName = kv.Key.FamilyNames[FontConstant.LanguageIdEnUs];
208 | if (!subsetFonts.TryGetValue(_famName, out var _))
209 | {
210 | subsetFonts.Add(_famName, []);
211 | }
212 | subsetFonts[_famName].Add(new SubsetFont(new FileInfo(kv.Key.FileName), kv.Key.Index, horRunes, vertRunes));
213 | }
214 | logger?.ZLogDebug($"Convert completed");
215 |
216 | _stopwatch.Stop();
217 | logger?.ZLogInformation($"Generate completed, use {_stopwatch.ElapsedMilliseconds} ms");
218 | _stopwatch.Reset();
219 | return subsetFonts;
220 | }
221 |
222 | static void ChangeAssFontName(AssData ass, Dictionary nameMap, Dictionary> fontMap)
223 | {
224 | // map ass font name and random name
225 | Dictionary assFontNameMap = [];
226 | foreach (var (kv, kv2) in from kv in nameMap
227 | from kv2 in fontMap
228 | where kv2.Key.FamilyNames.ContainsValue(kv.Key)
229 | select (kv, kv2))
230 | {
231 | foreach (var afi in kv2.Value)
232 | {
233 | assFontNameMap.TryAdd(afi.Name.StartsWith('@') ? afi.Name[1..] : afi.Name, kv.Value);
234 | }
235 | }
236 |
237 | foreach (var style in ass.Styles.Collection)
238 | {
239 | if (assFontNameMap.TryGetValue(style.Fontname, out var newFn))
240 | {
241 | style.Fontname = newFn;
242 | }
243 | }
244 |
245 | var assFontNameMapSort = assFontNameMap.OrderByDescending(d => d.Key).ToDictionary();
246 |
247 | var sb = new StringBuilder();
248 | foreach (var evt in ass.Events.Collection)
249 | {
250 | if (!evt.IsDialogue) { continue; }
251 | var text = evt.Text.AsSpan();
252 | if (text.IsEmpty) { continue; }
253 |
254 | if (evt.TextRanges.Length == 0)
255 | {
256 | evt.UpdateTextRanges();
257 | }
258 |
259 | if (evt.TextRanges.Length == 1)
260 | {
261 | continue;
262 | }
263 |
264 | var lineChanged = false;
265 | foreach (var range in evt.TextRanges)
266 | {
267 | var block = text[range];
268 | Debug.WriteLine($"{range.Start}:{range.End}:{block}");
269 | if (AssEvent.IsOverrideBlock(block))
270 | {
271 | if (ReplaceFontName(block, assFontNameMapSort, sb))
272 | {
273 | lineChanged = true;
274 | }
275 | }
276 | else
277 | {
278 | sb.Append(block);
279 | }
280 | Debug.WriteLine(sb.ToString());
281 | }
282 |
283 | if (lineChanged)
284 | {
285 | evt.Text = sb.ToString();
286 | }
287 |
288 | sb.Clear();
289 | }
290 |
291 | List subsetList = [];
292 | foreach (var kv in assFontNameMapSort)
293 | {
294 | subsetList.Add($"Font Subset: {kv.Value} - {kv.Key}");
295 | }
296 | subsetList.AddRange(ass.ScriptInfo.Comment);
297 | ass.ScriptInfo.Comment = subsetList;
298 | }
299 |
300 | private static bool ReplaceFontName(ReadOnlySpan block, Dictionary nameMap, StringBuilder sb)
301 | {
302 | var changed = false;
303 | var start = 0;
304 | var tagIndex = block.IndexOf($@"{AssConstants.BackSlash}{AssConstants.OverrideTags.FontName}");
305 | while (tagIndex != -1)
306 | {
307 | tagIndex += 3;
308 | sb.Append(block.Slice(start, tagIndex));
309 | start += tagIndex;
310 |
311 | var sepValues = SearchValues.Create($"{AssConstants.BackSlash}{AssConstants.EndOvrBlock}");
312 | var nextTag = block[start..].IndexOfAny(sepValues);
313 |
314 | var tagValue = nextTag == -1 ? block[start..] : block.Slice(start, nextTag);
315 |
316 | var matched = false;
317 | foreach (var (oldValue, newValue) in nameMap)
318 | {
319 | var vertical = tagValue.Length > 1 && tagValue[0] == '@';
320 | if (tagValue[(vertical ? 1 : 0)..].SequenceEqual(oldValue))
321 | {
322 | if (vertical)
323 | {
324 | sb.Append('@');
325 | }
326 | sb.Append(newValue);
327 | changed = true;
328 | matched = true;
329 | break;
330 | }
331 | }
332 |
333 | if (!matched)
334 | {
335 | sb.Append(tagValue);
336 | }
337 |
338 | start += tagValue.Length;
339 | if (nextTag == -1)
340 | {
341 | break;
342 | }
343 |
344 | tagIndex = block[start..].IndexOf($@"{AssConstants.BackSlash}{AssConstants.OverrideTags.FontName}");
345 | }
346 |
347 | sb.Append(block[start..]);
348 |
349 | return changed;
350 | }
351 | }
352 |
--------------------------------------------------------------------------------
/AssFontSubset.Core/src/SubsetFont.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace AssFontSubset.Core;
4 |
5 | public class SubsetFont(FileInfo originalFontFile, uint index, HashSet horRunes, HashSet vertRunes)
6 | {
7 | private readonly FileInfo _originalFontFile = originalFontFile;
8 | public FileInfo OriginalFontFile
9 | {
10 | get => _originalFontFile ?? throw new Exception("Please set OriginalFontFile fullpath");
11 | set
12 | {
13 | //_originalFontFile = value;
14 | if (value != null)
15 | {
16 | CharactersFile = Path.GetFileNameWithoutExtension(value.Name) + ".txt";
17 | }
18 | }
19 | }
20 |
21 | //public bool IsCollection = isCollection;
22 | public readonly uint TrackIndex = index;
23 |
24 | //public List? FontNameInAss;
25 | public HashSet Runes = [];
26 | private HashSet horizontalRunes = horRunes;
27 | private HashSet verticalRunes = vertRunes;
28 |
29 | //public string? OriginalFamilyName;
30 | public string? RandomNewName;
31 | public string? CharactersFile;
32 | public string? SubsetFontFileTemp;
33 | public string? SubsetFontTtxTemp;
34 | //public string? SubsetFontFile;
35 |
36 | public override int GetHashCode() => HashCode.Combine(_originalFontFile.Name, TrackIndex);
37 |
38 | public static string[] GenerateRandomStrings(int length, int count)
39 | {
40 | const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
41 | var random = new Random();
42 | var key = string.Empty;
43 | var keys = new HashSet(count);
44 | for (var i = 0; i < count; i++)
45 | {
46 | do
47 | {
48 | key = new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray());
49 | } while (!keys.Add(key));
50 | }
51 | var result = keys.ToArray();
52 | keys = null;
53 | return result;
54 | }
55 |
56 | public void Preprocessing()
57 | {
58 | //var chars = new List();
59 | //var emojis = new List();
60 | var runes = new HashSet();
61 |
62 | foreach (var rune in horizontalRunes)
63 | {
64 | if (rune.IsBmp)
65 | {
66 | if (Rune.IsDigit(rune) || char.IsAsciiLetter((char)rune.Value) || (rune.Value >= 0xFF10 && rune.Value <= 0xFF19))
67 | {
68 | continue;
69 | }
70 | runes.Add(rune);
71 | }
72 | else
73 | {
74 | runes.Add(rune);
75 | }
76 | }
77 |
78 | foreach (var rune in verticalRunes)
79 | {
80 | if (rune.IsBmp)
81 | {
82 | if (Rune.IsDigit(rune) || char.IsAsciiLetter((char)rune.Value) || (rune.Value >= 0xFF10 && rune.Value <= 0xFF19))
83 | {
84 | continue;
85 | }
86 | runes.Add(rune);
87 | if (FontConstant.VertMapping.TryGetValue((char)rune.Value, out var vertChar))
88 | {
89 | runes.Add(new Rune(vertChar));
90 | }
91 | }
92 | else
93 | {
94 | runes.Add(rune);
95 | }
96 | }
97 |
98 | AppendNecessaryRunes(runes);
99 | Runes = runes;
100 | }
101 |
102 | ///
103 | /// Subset all half-width and full-width letters and digits, will fix font fallback on ellipsis
104 | ///
105 | ///
106 | private static void AppendNecessaryRunes(HashSet runes)
107 | {
108 | // letters
109 | // Uppercase Latin alphabet
110 | for (var i = 0x0041; i <= 0x005A; i++)
111 | {
112 | runes.Add(new Rune(i));
113 | runes.Add(new Rune(i + 65248));
114 | }
115 |
116 | // I don’t know why
117 | runes.Add(new Rune(0xFF1F));
118 | runes.Add(new Rune(0xFF20));
119 |
120 | // Lowercase Latin alphabet
121 | for (var i = 0x0061; i <= 0x007A; i++)
122 | {
123 | runes.Add(new Rune(i));
124 | runes.Add(new Rune(i + 65248));
125 | }
126 |
127 | // digits
128 | for (var i = 0x0030; i <= 0x0039; i++)
129 | {
130 | runes.Add(new Rune(i));
131 | runes.Add(new Rune(i + 65248));
132 | }
133 | }
134 |
135 | public void WriteRunesToUtf8File()
136 | {
137 | using var fs = new FileStream(CharactersFile!, FileMode.Create, FileAccess.Write, FileShare.None);
138 | var buffer = new byte[1024];
139 | int bufferOffset = 0;
140 |
141 | foreach (var rune in Runes)
142 | {
143 | if (bufferOffset + 4 > buffer.Length)
144 | {
145 | fs.Write(buffer, 0, bufferOffset);
146 | bufferOffset = 0;
147 | }
148 |
149 | var bytesWritten = rune.EncodeToUtf8(buffer.AsSpan(bufferOffset));
150 | bufferOffset += bytesWritten;
151 | }
152 |
153 | if (bufferOffset > 0)
154 | {
155 | fs.Write(buffer, 0, bufferOffset);
156 | }
157 |
158 | fs.Flush();
159 | fs.Close();
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/AssFontSubset.Core/src/SubsetToolBase.cs:
--------------------------------------------------------------------------------
1 | namespace AssFontSubset.Core;
2 |
3 | public abstract class SubsetToolBase
4 | {
5 | public abstract void SubsetFonts(Dictionary> subsetFonts, string outputFolder, out Dictionary nameMap);
6 | public abstract void CreateFontSubset(SubsetFont ssf, string outputFolder);
7 | }
--------------------------------------------------------------------------------
/AssFontSubset.CoreTests/AssFontSubset.Core.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | enable
5 |
6 | false
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 | all
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/AssFontSubset.CoreTests/src/AssFontTests.cs:
--------------------------------------------------------------------------------
1 | using Mobsub.SubtitleParse.AssTypes;
2 | using static AssFontSubset.Core.AssFont;
3 |
4 | namespace AssFontSubset.Core.Tests;
5 |
6 | [TestClass]
7 | public class AssFontTests
8 | {
9 | [TestMethod]
10 | public void MatchTestTrueBIZ()
11 | {
12 | var fn = "Times New Roman";
13 | // var fnChs = "Times New Roman";
14 | var fnDict = new Dictionary()
15 | {
16 | {1033, fn},
17 | };
18 |
19 | var afiR = new AssFontInfo() { Name = fn, Weight = 0, Italic = false };
20 | var afiB = new AssFontInfo() { Name = fn, Weight = 1, Italic = false };
21 | var afiI = new AssFontInfo() { Name = fn, Weight = 0, Italic = true };
22 | var afiZ = new AssFontInfo() { Name = fn, Weight = 1, Italic = true };
23 | var afi4 = new AssFontInfo() { Name = fn, Weight = 400, Italic = false };
24 |
25 | var fiR = new FontInfo() { FamilyNames = fnDict, Bold = false, Italic = false, Weight = 400 };
26 | var fiB = new FontInfo() { FamilyNames = fnDict, Bold = true, Italic = false, Weight = 700 };
27 | var fiI = new FontInfo() { FamilyNames = fnDict, Bold = false, Italic = true, Weight = 400 };
28 | var fiZ = new FontInfo() { FamilyNames = fnDict, Bold = true, Italic = true, Weight = 700 };
29 |
30 | var afL = new List() { afiR, afiB, afiI, afiZ, afi4 };
31 | var fiL = new List() { fiR, fiB, fiI, fiZ };
32 | var fiGroups = fiL.GroupBy(fontInfo => fontInfo.FamilyNames[1033]);
33 |
34 | foreach (var a in afL)
35 | {
36 | foreach (var f in fiGroups)
37 | {
38 | var tfi = GetMatchedFontInfo(a, f);
39 | if (a == afiR) { Assert.IsTrue(fiR == tfi); }
40 | if (a == afiB) { Assert.IsTrue(fiB == tfi); }
41 | if (a == afiI) { Assert.IsTrue(fiI == tfi); }
42 | if (a == afiZ) { Assert.IsTrue(fiZ == tfi); }
43 | if (a == afi4) { Assert.IsTrue(fiR == tfi); }
44 | }
45 | }
46 | }
47 |
48 | [TestMethod]
49 | public void MatchTestFakeBIZ()
50 | {
51 | var fn = "FZLanTingHei-R-GBK";
52 | var fnChs = "方正兰亭黑_GBK";
53 | var fnDict = new Dictionary()
54 | {
55 | {1033, fn},
56 | {2052, fnChs}
57 | };
58 |
59 | var afi = new AssFontInfo() { Name = fn, Weight = 0, Italic = false };
60 | var afiBF = new AssFontInfo() { Name = fn, Weight = 1, Italic = false };
61 | var afiIF = new AssFontInfo() { Name = fn, Weight = 0, Italic = true };
62 | var afiZF = new AssFontInfo() { Name = fn, Weight = 1, Italic = true };
63 | var afiChs = new AssFontInfo() { Name = fnChs, Weight = 0, Italic = false };
64 | var afiBFChs = new AssFontInfo() { Name = fnChs, Weight = 1, Italic = false };
65 | var afiIFChs = new AssFontInfo() { Name = fnChs, Weight = 0, Italic = true };
66 | var afiZFChs = new AssFontInfo() { Name = fnChs, Weight = 1, Italic = true };
67 |
68 | var fi = new FontInfo() { FamilyNames = fnDict, Bold = false, Italic = false, Weight = 400 };
69 |
70 | var afL = new List() { afi, afiBF, afiIF, afiZF, afiChs, afiBFChs, afiIFChs, afiZFChs };
71 | foreach ( var af in afL )
72 | {
73 | Assert.IsTrue(IsMatch(af, fi, true));
74 | }
75 | }
76 |
77 | [TestMethod]
78 | public void MatchTestPartiallyTrueBIZ()
79 | {
80 | var fn = "Times New Roman";
81 | var fnChs = "Times New Roman";
82 | var fnDict = new Dictionary()
83 | {
84 | {1033, fn},
85 | {2052, fnChs}
86 | };
87 |
88 | var afiR = new AssFontInfo() { Name = fn, Weight = 0, Italic = false };
89 | var afiB = new AssFontInfo() { Name = fn, Weight = 1, Italic = false };
90 | var afiI = new AssFontInfo() { Name = fn, Weight = 0, Italic = true };
91 | var afiZ = new AssFontInfo() { Name = fn, Weight = 1, Italic = true };
92 | var afi4 = new AssFontInfo() { Name = fn, Weight = 400, Italic = false };
93 |
94 | //var fiR = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = false, Italic = false, Weight = 400 };
95 | //var fiB = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = true, Italic = false, Weight = 700 };
96 | var fiI = new FontInfo() { FamilyNames = fnDict, Bold = false, Italic = true, Weight = 400 };
97 | var fiZ = new FontInfo() { FamilyNames = fnDict, Bold = true, Italic = true, Weight = 700 };
98 |
99 | var afL = new List() { afiR, afiB, afiI, afiZ, afi4 };
100 | var fiL = new List() { fiI, fiZ };
101 | var fiGroups = fiL.GroupBy(fontInfo => fontInfo.FamilyNames[1033]);
102 |
103 | foreach (var a in afL)
104 | {
105 | foreach (var f in fiGroups)
106 | {
107 | var tfi = GetMatchedFontInfo(a, f);
108 | if (a == afiR) { Assert.IsTrue(null == tfi); }
109 | if (a == afiB) { Assert.IsTrue(null == tfi); }
110 | if (a == afiI) { Assert.IsTrue(fiI == tfi); }
111 | if (a == afiZ) { Assert.IsTrue(fiZ == tfi); }
112 | if (a == afi4) { Assert.IsTrue(null == tfi); }
113 | }
114 | }
115 | }
116 |
117 | [TestMethod]
118 | public void MatchTestTrueB()
119 | {
120 | var fn = "Source Han Sans";
121 | var fnChs = "思源黑体";
122 | var fnDict = new Dictionary()
123 | {
124 | {1033, fn},
125 | {2052, fnChs}
126 | };
127 |
128 | var afiR = new AssFontInfo() { Name = fn, Weight = 0, Italic = false };
129 | var afiB = new AssFontInfo() { Name = fn, Weight = 1, Italic = false };
130 | var afiI = new AssFontInfo() { Name = fn, Weight = 0, Italic = true };
131 | var afiZ = new AssFontInfo() { Name = fn, Weight = 1, Italic = true };
132 | var afi4 = new AssFontInfo() { Name = fn, Weight = 400, Italic = false };
133 |
134 | var fiR = new FontInfo() { FamilyNames = fnDict, Bold = false, Italic = false, Weight = 400, MaxpNumGlyphs = 65535 };
135 | var fiB = new FontInfo() { FamilyNames = fnDict, Bold = true, Italic = false, Weight = 700, MaxpNumGlyphs = 65535 };
136 |
137 | var afL = new List() { afiR, afiB, afiI, afiZ, afi4 };
138 | var fiL = new List() { fiR, fiB };
139 | var fiGroups = fiL.GroupBy(fontInfo => fontInfo.FamilyNames[1033]);
140 |
141 | foreach (var a in afL)
142 | {
143 | foreach (var f in fiGroups)
144 | {
145 | var tfi = GetMatchedFontInfo(a, f);
146 | if (a == afiR) { Assert.IsTrue(fiR == tfi); }
147 | if (a == afiB) { Assert.IsTrue(fiB == tfi); }
148 | if (a == afiI) { Assert.IsTrue(fiR == tfi); }
149 | if (a == afiZ) { Assert.IsTrue(fiB == tfi); }
150 | if (a == afi4) { Assert.IsTrue(fiR == tfi); }
151 | }
152 | }
153 | }
154 | }
--------------------------------------------------------------------------------
/AssFontSubset.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.9.34723.18
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssFontSubset.Console", "AssFontSubset.Console\AssFontSubset.Console.csproj", "{D53841EE-4B6C-434F-84C6-AF6C4AB21281}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssFontSubset.Core", "AssFontSubset.Core\AssFontSubset.Core.csproj", "{92B1A0C8-EB23-43F1-A0C6-09583DB19469}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssFontSubset.Avalonia", "AssFontSubset.Avalonia\AssFontSubset.Avalonia.csproj", "{228541CF-C1A6-48C5-8B60-95538E049C25}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssFontSubset.Core.Tests", "AssFontSubset.CoreTests\AssFontSubset.Core.Tests.csproj", "{E003AA3B-7C95-4FA3-AB7D-529B71BBD85D}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {D53841EE-4B6C-434F-84C6-AF6C4AB21281}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {D53841EE-4B6C-434F-84C6-AF6C4AB21281}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {D53841EE-4B6C-434F-84C6-AF6C4AB21281}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {D53841EE-4B6C-434F-84C6-AF6C4AB21281}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {92B1A0C8-EB23-43F1-A0C6-09583DB19469}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {92B1A0C8-EB23-43F1-A0C6-09583DB19469}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {92B1A0C8-EB23-43F1-A0C6-09583DB19469}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {92B1A0C8-EB23-43F1-A0C6-09583DB19469}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {228541CF-C1A6-48C5-8B60-95538E049C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {228541CF-C1A6-48C5-8B60-95538E049C25}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {228541CF-C1A6-48C5-8B60-95538E049C25}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {228541CF-C1A6-48C5-8B60-95538E049C25}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {E003AA3B-7C95-4FA3-AB7D-529B71BBD85D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {E003AA3B-7C95-4FA3-AB7D-529B71BBD85D}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {E003AA3B-7C95-4FA3-AB7D-529B71BBD85D}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {E003AA3B-7C95-4FA3-AB7D-529B71BBD85D}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {7AAEC8D3-F3CE-4178-9BC0-5399DA0548AE}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/AssFontSubset.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/HarfBuzzBinding/HarfBuzzBinding.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | latest
6 | 0.1.0
7 | enable
8 | enable
9 | true
10 |
11 |
12 |
13 | Win
14 |
15 |
16 | Linux
17 |
18 |
19 | Mac
20 |
21 |
22 |
23 | Win
24 |
25 |
26 | Linux
27 |
28 |
29 | Mac
30 |
31 |
32 |
33 | Shared
34 |
35 |
36 | Static
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/HarfBuzzBinding/README.md:
--------------------------------------------------------------------------------
1 | # HarfBuzzBinding
2 |
3 | This is a binding library for HarfBuzz, primarily targeting the use of `harfbuzz-subset`.
4 |
5 | The dependent native libraries are provided by `MIR.NativeLib.Harfbuzz.*`, shared libraries are used for debugging, and static libraries are employed for release builds, so you can only `PublishAot` when publish release. The version of `MIR.NativeLib.Harfbuzz.*` is harfbuzz version + package patch version. Currently, it only supports the following RIDs:
6 |
7 | - Windows:
8 | - win-x64
9 | - win-arm64
10 | - Linux:
11 | - linux-musl-x64
12 | - linux-musl-arm64
13 | - Mac:
14 | - osx-x64
15 | - osx-arm64
--------------------------------------------------------------------------------
/HarfBuzzBinding/src/Methods.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using System.Text;
3 | using HarfBuzzBinding.Native;
4 | using SubsetApis = HarfBuzzBinding.Native.Subset.Apis;
5 | using HBApis = HarfBuzzBinding.Native.Apis;
6 |
7 | namespace HarfBuzzBinding;
8 |
9 | public unsafe class Methods
10 | {
11 | public static string? GetHarfBuzzVersion() => Marshal.PtrToStringAnsi((IntPtr)HBApis.hb_version_string());
12 |
13 | public static bool TryGetFontFace(string fontFile, uint faceIndex, out hb_face_t* face)
14 | {
15 | var harfBuzzVersion = new Version(GetHarfBuzzVersion()!);
16 |
17 | if (harfBuzzVersion is { Major: >= 10, Minor: >= 1 })
18 | {
19 | face = HBApis.hb_face_create_from_file_or_fail((sbyte*)Marshal.StringToHGlobalAnsi(fontFile), faceIndex);
20 | return (IntPtr)face != IntPtr.Zero;
21 | }
22 |
23 | var blob = HBApis.hb_blob_create_from_file_or_fail((sbyte*)Marshal.StringToHGlobalAnsi(fontFile));
24 | if ((IntPtr)blob == IntPtr.Zero)
25 | {
26 | face = (hb_face_t*)IntPtr.Zero;
27 | return false;
28 | }
29 |
30 | face = HBApis.hb_face_create(blob, faceIndex);
31 | HBApis.hb_blob_destroy(blob);
32 | return true;
33 | }
34 |
35 | public static void WriteFontFile(hb_blob_t* blob, string destFile)
36 | {
37 | uint length;
38 | var dataPtr = HBApis.hb_blob_get_data(blob, &length);
39 | var stream = new UnmanagedMemoryStream((byte*)dataPtr, length);
40 |
41 | using var fileStream = new FileStream(destFile, FileMode.Create);
42 | stream.CopyTo(fileStream);
43 | stream.Dispose();
44 |
45 | HBApis.hb_blob_destroy(blob);
46 | }
47 |
48 | public static void RenameFontname(hb_subset_input_t* input, sbyte* versionString, sbyte* nameString, OpenTypeNameId[] ids)
49 | {
50 | foreach (var id in ids)
51 | {
52 | _ = SubsetApis.hb_subset_input_override_name_table(input, id.NameId, id.PlatformId, id.EncodingId, id.LanguageId, id.NameId == 0 ? versionString : nameString, -1);
53 | }
54 | }
55 | }
56 |
57 | public struct OpenTypeNameId
58 | {
59 | public uint NameId;
60 | public uint PlatformId;
61 | public uint LanguageId;
62 | public uint EncodingId;
63 | }
--------------------------------------------------------------------------------
/HarfBuzzBinding/src/Native/Apis.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using static HarfBuzzBinding.Native.Library;
3 | // ReSharper disable InconsistentNaming
4 |
5 | namespace HarfBuzzBinding.Native;
6 |
7 | public static unsafe partial class Apis
8 | {
9 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
10 | public static extern sbyte* hb_version_string();
11 |
12 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
13 | public static extern hb_blob_t* hb_blob_create_from_file_or_fail(sbyte* file_name);
14 |
15 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
16 | public static extern void* hb_blob_get_data(hb_blob_t* blob, uint* length);
17 |
18 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
19 | public static extern void hb_blob_destroy(hb_blob_t* blob);
20 |
21 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
22 | public static extern hb_face_t* hb_face_create(hb_blob_t* blob, uint index);
23 |
24 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
25 | public static extern hb_face_t* hb_face_create_from_file_or_fail(sbyte* file_name, uint index);
26 |
27 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
28 | public static extern hb_blob_t* hb_face_reference_blob(hb_face_t* face);
29 |
30 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
31 | public static extern void hb_face_destroy(hb_face_t* face);
32 |
33 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
34 | public static extern void hb_set_add(hb_set_t* set, [NativeTypeName("hb_codepoint_t")] uint codepoint);
35 |
36 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
37 | public static extern void hb_set_clear(hb_set_t* set);
38 |
39 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
40 | public static extern void hb_set_destroy(hb_set_t* set);
41 |
42 | [DllImport(HarfBuzzDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
43 | [return: NativeTypeName("hb_tag_t")]
44 | public static extern uint hb_tag_from_string(sbyte* str, int len);
45 | }
--------------------------------------------------------------------------------
/HarfBuzzBinding/src/Native/Library.cs:
--------------------------------------------------------------------------------
1 | global using hb_blob_t = System.IntPtr;
2 | global using hb_buffer_t = System.IntPtr;
3 | global using hb_face_t = System.IntPtr;
4 | global using hb_font_funcs_t = System.IntPtr;
5 | global using hb_font_t = System.IntPtr;
6 | global using hb_language_impl_t = System.IntPtr;
7 | global using hb_map_t = System.IntPtr;
8 | global using hb_set_t = System.IntPtr;
9 | global using hb_shape_plan_t = System.IntPtr;
10 | global using hb_unicode_funcs_t = System.IntPtr;
11 |
12 | namespace HarfBuzzBinding.Native;
13 |
14 | internal static class Library
15 | {
16 | internal const string HarfBuzzDll = "harfbuzz";
17 | internal const string HarfBuzzSubsetDll = "harfbuzz-subset";
18 | }
--------------------------------------------------------------------------------
/HarfBuzzBinding/src/Native/NativeTypeNameAttribute.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace HarfBuzzBinding.Native
4 | {
5 | /// Defines the type of a member as it was used in the native signature.
6 | [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = false, Inherited = true)]
7 | [Conditional("DEBUG")]
8 | internal sealed partial class NativeTypeNameAttribute : Attribute
9 | {
10 | private readonly string _name;
11 |
12 | /// Initializes a new instance of the class.
13 | /// The name of the type that was used in the native signature.
14 | public NativeTypeNameAttribute(string name)
15 | {
16 | _name = name;
17 | }
18 |
19 | /// Gets the name of the type that was used in the native signature.
20 | public string Name => _name;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/HarfBuzzBinding/src/Native/Subset/Apis.cs:
--------------------------------------------------------------------------------
1 | global using hb_subset_input_t = System.IntPtr;
2 | global using hb_subset_plan_t = System.IntPtr;
3 | using System.Runtime.InteropServices;
4 | using static HarfBuzzBinding.Native.Library;
5 | // ReSharper disable InconsistentNaming
6 |
7 | namespace HarfBuzzBinding.Native.Subset
8 | {
9 | public static unsafe partial class Apis
10 | {
11 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
12 | public static extern hb_subset_input_t* hb_subset_input_create_or_fail();
13 |
14 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
15 | public static extern hb_subset_input_t* hb_subset_input_reference(hb_subset_input_t* input);
16 |
17 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
18 | public static extern void hb_subset_input_destroy(hb_subset_input_t* input);
19 |
20 | // [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
21 | // [return: NativeTypeName("hb_bool_t")]
22 | // public static extern int hb_subset_input_set_user_data(hb_subset_input_t* input, hb_user_data_key_t* key, void* data, [NativeTypeName("hb_destroy_func_t")] delegate* unmanaged[Cdecl] destroy, [NativeTypeName("hb_bool_t")] int replace);
23 |
24 | // [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
25 | // public static extern void* hb_subset_input_get_user_data([NativeTypeName("const hb_subset_input_t *")] hb_subset_input_t* input, hb_user_data_key_t* key);
26 |
27 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
28 | public static extern void hb_subset_input_keep_everything(hb_subset_input_t* input);
29 |
30 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
31 | public static extern hb_set_t* hb_subset_input_unicode_set(hb_subset_input_t* input);
32 |
33 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
34 | public static extern hb_set_t* hb_subset_input_glyph_set(hb_subset_input_t* input);
35 |
36 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
37 | public static extern hb_set_t* hb_subset_input_set(hb_subset_input_t* input, hb_subset_sets_t set_type);
38 |
39 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
40 | public static extern hb_map_t* hb_subset_input_old_to_new_glyph_mapping(hb_subset_input_t* input);
41 |
42 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
43 | public static extern hb_subset_flags_t hb_subset_input_get_flags(hb_subset_input_t* input);
44 |
45 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
46 | public static extern void hb_subset_input_set_flags(hb_subset_input_t* input, [NativeTypeName("unsigned int")] uint value);
47 |
48 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
49 | [return: NativeTypeName("hb_bool_t")]
50 | public static extern int hb_subset_input_pin_all_axes_to_default(hb_subset_input_t* input, hb_face_t* face);
51 |
52 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
53 | [return: NativeTypeName("hb_bool_t")]
54 | public static extern int hb_subset_input_pin_axis_to_default(hb_subset_input_t* input, hb_face_t* face, [NativeTypeName("hb_tag_t")] uint axis_tag);
55 |
56 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
57 | [return: NativeTypeName("hb_bool_t")]
58 | public static extern int hb_subset_input_pin_axis_location(hb_subset_input_t* input, hb_face_t* face, [NativeTypeName("hb_tag_t")] uint axis_tag, float axis_value);
59 |
60 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
61 | public static extern hb_face_t* hb_subset_preprocess(hb_face_t* source);
62 |
63 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
64 | public static extern hb_face_t* hb_subset_or_fail(hb_face_t* source, [NativeTypeName("const hb_subset_input_t *")] hb_subset_input_t* input);
65 |
66 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
67 | public static extern hb_face_t* hb_subset_plan_execute_or_fail(hb_subset_plan_t* plan);
68 |
69 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
70 | public static extern hb_subset_plan_t* hb_subset_plan_create_or_fail(hb_face_t* face, [NativeTypeName("const hb_subset_input_t *")] hb_subset_input_t* input);
71 |
72 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
73 | public static extern void hb_subset_plan_destroy(hb_subset_plan_t* plan);
74 |
75 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
76 | public static extern hb_map_t* hb_subset_plan_old_to_new_glyph_mapping([NativeTypeName("const hb_subset_plan_t *")] hb_subset_plan_t* plan);
77 |
78 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
79 | public static extern hb_map_t* hb_subset_plan_new_to_old_glyph_mapping([NativeTypeName("const hb_subset_plan_t *")] hb_subset_plan_t* plan);
80 |
81 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
82 | public static extern hb_map_t* hb_subset_plan_unicode_to_old_glyph_mapping([NativeTypeName("const hb_subset_plan_t *")] hb_subset_plan_t* plan);
83 |
84 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
85 | public static extern hb_subset_plan_t* hb_subset_plan_reference(hb_subset_plan_t* plan);
86 |
87 | // [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
88 | // [return: NativeTypeName("hb_bool_t")]
89 | // public static extern int hb_subset_plan_set_user_data(hb_subset_plan_t* plan, hb_user_data_key_t* key, void* data, [NativeTypeName("hb_destroy_func_t")] delegate* unmanaged[Cdecl] destroy, [NativeTypeName("hb_bool_t")] int replace);
90 | //
91 | // [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
92 | // public static extern void* hb_subset_plan_get_user_data([NativeTypeName("const hb_subset_plan_t *")] hb_subset_plan_t* plan, hb_user_data_key_t* key);
93 |
94 | // HB_EXPERIMENTAL_API
95 | [DllImport(HarfBuzzSubsetDll, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
96 | [return: NativeTypeName("hb_bool_t")]
97 | public static extern int hb_subset_input_override_name_table(hb_subset_input_t* input, [NativeTypeName("hb_ot_name_id_t")] uint name_id, [NativeTypeName("unsigned int")] uint platform_id, [NativeTypeName("unsigned int")] uint encoding_id, [NativeTypeName("unsigned int")] uint language_id, [NativeTypeName("const char *")] sbyte* name_str, int str_len);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/HarfBuzzBinding/src/Native/Subset/hb_subset_flags_t.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable InconsistentNaming
2 |
3 | namespace HarfBuzzBinding.Native.Subset
4 | {
5 | public enum hb_subset_flags_t : uint
6 | {
7 | HB_SUBSET_FLAGS_DEFAULT = 0x00000000U,
8 | HB_SUBSET_FLAGS_NO_HINTING = 0x00000001U,
9 | HB_SUBSET_FLAGS_RETAIN_GIDS = 0x00000002U,
10 | HB_SUBSET_FLAGS_DESUBROUTINIZE = 0x00000004U,
11 | HB_SUBSET_FLAGS_NAME_LEGACY = 0x00000008U,
12 | HB_SUBSET_FLAGS_SET_OVERLAPS_FLAG = 0x00000010U,
13 | HB_SUBSET_FLAGS_PASSTHROUGH_UNRECOGNIZED = 0x00000020U,
14 | HB_SUBSET_FLAGS_NOTDEF_OUTLINE = 0x00000040U,
15 | HB_SUBSET_FLAGS_GLYPH_NAMES = 0x00000080U,
16 | HB_SUBSET_FLAGS_NO_PRUNE_UNICODE_RANGES = 0x00000100U,
17 | HB_SUBSET_FLAGS_NO_LAYOUT_CLOSURE = 0x00000200U,
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/HarfBuzzBinding/src/Native/Subset/hb_subset_sets_t.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable InconsistentNaming
2 |
3 | namespace HarfBuzzBinding.Native.Subset
4 | {
5 | public enum hb_subset_sets_t
6 | {
7 | HB_SUBSET_SETS_GLYPH_INDEX = 0,
8 | HB_SUBSET_SETS_UNICODE,
9 | HB_SUBSET_SETS_NO_SUBSET_TABLE_TAG,
10 | HB_SUBSET_SETS_DROP_TABLE_TAG,
11 | HB_SUBSET_SETS_NAME_ID,
12 | HB_SUBSET_SETS_NAME_LANG_ID,
13 | HB_SUBSET_SETS_LAYOUT_FEATURE_TAG,
14 | HB_SUBSET_SETS_LAYOUT_SCRIPT_TAG,
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AssFontSubset
2 |
3 | 使用 fonttools 或 harfbuzz-subset 生成 ASS 字幕文件的字体子集,并自动修改字体名称及 ASS 文件中对应的字体名称
4 |
5 | ## 依赖
6 |
7 | 如果使用 fonttools 进行子集化,需要:
8 | 1. [fonttools](https://github.com/fonttools/fonttools),推荐使用最新版本
9 | 2. Path 环境变量中存在 pyftsubset 和 ttx,也可以通过选项指定所在目录,此二者是 fonttools 的一部分
10 |
11 | ## AssFontSubset.Console
12 |
13 | ```
14 | Usage:
15 | AssFontSubset.Console [...] [options]
16 |
17 | Arguments:
18 | 要子集化的 ASS 字幕文件路径,可以输入多个同目录的字幕文件
19 |
20 | Options:
21 | -?, -h, --help Show help and usage information
22 | --version Show version information
23 | --fonts ASS 字幕文件需要的字体所在目录,默认为 ASS 同目录的 fonts 文件夹
24 | --output 子集化后成品所在目录,默认为 ASS 同目录的 output 文件夹
25 | --subset-backend 子集化使用的后端 [default: PyFontTools]
26 | --bin-path 指定 pyftsubset 和 ttx 所在目录。若未指定,会使用环境变量中的
27 | --source-han-ellipsis 使思源黑体和宋体的省略号居中对齐 [default: True]
28 | --debug 保留子集化期间的各种临时文件,位于 --output-dir 指定的文件夹;同时打印 出所有运行的命令 [default: False]
29 | ```
30 |
31 | ## AssFontSubset.Avalonia
32 |
33 | 大致可以参考 [v1 README](./README_v1.md) 中的 gui 使用方法,目前不支持显示实时的子集化进度和 harfbuzz-subset 后端。
34 |
35 | ## 注意
36 |
37 | 1. 每次生成时会自动删除 ass 同目录下的 output 文件夹。
38 | 2. 目前,子集化后只保留了必要的 OpenType features,可参照[此 issue](https://github.com/AmusementClub/AssFontSubset/issues/13) 和 [保留的 features](https://github.com/AmusementClub/AssFontSubset/blob/b9e872b2ae450001eada6e84f47a32198a3c11a7/AssFontSubset.Core/src/FontConstant.cs#L49-L63)。
39 |
40 | ## Todo
41 |
42 | 1. 考虑移除 gui 支持
43 | 2. 考虑增加对 fontations subset (klippa) 的支持
44 | 3. 不确定是否要支持可变字体(variable fonts)
45 | 4. 不确定是否要恢复检查更新的功能
46 |
47 | ## FAQ 常见问题和故障排除
48 |
49 | 1. 如果弹出的错误信息中有提到`请尝试使用 FontForge 重新生成字体。`: 请下载并安装 [Fontforge](https://fontforge.org/en-US/),然后使用 Fontforge 打开有问题的字体,不需要改动任何信息,直接点文件——生成字体(File - Generate Font),然后生成一个新的字体文件,无视中途弹出的警告。再使用新生成的字体进行子集化操作。
50 |
51 | 2. 如果 Fontforge 无法解决问题,或出现奇怪的错误,且没有有用的错误信息,请尝试更新 fonttools:
52 |
53 | ```
54 | pip3 install --upgrade fonttools
55 | ```
56 |
57 | 3. 其他已知问题
58 |
59 | - [部分特殊字体 hb-subset 丢弃而不是重生成可用的 cmap 表](https://github.com/harfbuzz/harfbuzz/issues/4980),目前只能切换到 fonttools 进行子集化
60 | - [方正锐正黑_GBK Bold 名称匹配错误](https://github.com/AmusementClub/AssFontSubset/issues/21)
61 |
62 | 4. 若有无法解决的问题,欢迎回报 issue
63 |
--------------------------------------------------------------------------------
/README_v1.md:
--------------------------------------------------------------------------------
1 | # AssFontSubset
2 | 使用 fonttools 生成 ASS 字幕文件的字体子集,并自动修改字体名称及 ASS 文件中对应的字体名称
3 |
4 | ## 依赖
5 | 1. [fonttools](https://github.com/fonttools/fonttools)
6 | 2. Path 环境变量中存在 pyftsubset.exe 和 ttx.exe
7 |
8 | ## 使用方法
9 |
10 | ### 基本使用方法
11 |
12 | 1. 建立一个新目录,放入 .ass 文件
13 | 2. 在新建立的目录中创建 fonts 目录,放入字体文件
14 | 3. 将 .ass 文件拖入窗口中,点击开始
15 | 4. 程序会自动生成 output 目录并放入修改后的 .ass 文件及子集化的字体文件
16 |
17 | #### 注意:
18 | 1. 每次生成时会自动删除 ass 同目录下的 output 文件夹。
19 |
20 | ### 选项
21 |
22 | 1. 居中思源省略号
23 |
24 | 默认:打开
25 |
26 | 思源黑体和宋体的中文省略号在某些特殊的情况下会变成变成类似 ... 的下对齐。如果不打开此选项,子集化后的所有的省略号都变成下对齐。打开后,所有的省略号会被居中对齐。
27 |
28 | 2. 调试选项
29 |
30 | 默认:关闭
31 |
32 | 打开后会保留各种临时文件,用于检查字体名字等在各个阶段是否正确。
33 |
34 | 3. 使用云跳过列表
35 |
36 | 默认:打开。
37 |
38 | 打开后会尝试读取 GitHub 上已判明子集化后会出现问题的字体列表,并将其跳过不进行处理。
39 |
40 | 使用此功能必须要可以连接到 GitHub。
41 |
42 | 欢迎大家报告子集化后有问题的字体。
43 |
44 | 4. 使用本地跳过列表
45 |
46 | 默认:打开
47 |
48 | 使用方法:在 exe 文件同目录下使用 utf-8 编码创建 skiplist.txt,然后将自己想要跳过,不进行子集化处理的字体名字填入其中,以换行分割。
49 |
50 | 注意
51 | - 要填写 ass 文件中使用的名字。(例:如果你在 ass 中使用的 `Source Han Sans SC Medium`,那该文件中也要填写相同的名字,而不能填写 `思源黑体 Medium`)。
52 | - 程序依旧会把跳过的未经子集化的字体复制到 output 文件夹下,该行为是为了便于自动化 remux。
53 | - 如果跳过的字体是属于一个 ttc,那整个 ttc 都会被复制到 output 文件夹下。
54 |
55 | 示例 skiplist.txt:
56 | ```
57 | Source Han Sans SC Medium
58 | 思源黑体 Medium
59 | 方正兰亭圆_GBK_特
60 | A-OTF Shin Maru Go Pr6N H
61 | ```
62 |
63 | ### 命令行
64 |
65 | `assfontsubset [subtitle files]`
66 |
67 | 用命令行调用程序时,只需要把 .ass 文件名作为参数输入进去即可,支持多个文件。
68 |
69 | `assfontsubset a.ass b.ass c.ass`
70 |
71 | 命令行模式暂不支持设置其他选项,使用命令行模式前请先手动打开程序 GUI 配置成想要的选项,程序关闭时会自动记忆选项配置。
72 |
73 |
74 |
75 | ## Todo
76 |
77 | 1. 多线程查找字体名
78 |
79 | ## FAQ 常见问题和故障排除
80 |
81 | 1. 如果弹出的错误信息中有提到`请尝试使用 FontForge 重新生成字体。`: 请下载并安装 [Fontforge](https://fontforge.org/en-US/),然后使用 Fontforge 打开有问题的字体,不需要改动任何信息,直接点文件——生成字体(File - Generate Font),然后生成一个新的字体文件,无视中途弹出的警告。再使用新生成的字体进行子集化操作。
82 |
83 |
84 | 2. 如果 Fontforge 无法解决问题,或出现奇怪的错误,且没有有用的错误信息,请尝试更新 fonttools:
85 |
86 | ```
87 | pip3 install --upgrade fonttools
88 | ```
89 |
90 | 3. 其他已知问题:
91 | [在竖排字体的符号可能会出现问题](https://github.com/tastysugar/AssFontSubset/issues/5)
92 | [一些 otf 字体竖排时子集化后,字体大小在 vsfilter 中显示不正常](https://github.com/tastysugar/AssFontSubset/issues/2)
--------------------------------------------------------------------------------
/build/global.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0;net9.0
5 | 2.1.1
6 | enable
7 | false
8 |
9 |
10 |
--------------------------------------------------------------------------------
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------