Skip to content

Commit

Permalink
Feat/hb-subset support (#22)
Browse files Browse the repository at this point in the history
* add HarfBuzzBinding

* split SubsetConfig from PyFontTools and add SubsetToolBase

* add HarfBuzzSubset

* rename SubsetByPyFT to SubsetCore

* afs.cmd: add new option --subset-backend

* HarfBuzzBinding: native apis fix typo

* afs.cmd: add missing

* HarfBuzzSubset: fix output font filename

* now only win-x64 support hb-subset

* HarfBuzzSubset: rename all need modfied names

* HarfBuzzSubset: fix debug chars txt is empty

* move enableFeatures to FontConstant.SubsetKeepFeatures

* remove unused layout features

* debug txt delete duplicate suffix

* afs.cmd: update --subset-backend description
  • Loading branch information
MIRIMIRIM authored Nov 10, 2024
1 parent 7ae8c0c commit f0579b5
Show file tree
Hide file tree
Showing 20 changed files with 531 additions and 37 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,5 @@ __pycache__/

# Test Files
test/
*/Properties/*
*/Properties/*
/native
2 changes: 1 addition & 1 deletion AssFontSubset.Avalonia/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ private async Task AssFontSubsetByPyFT(FileInfo[] path, DirectoryInfo? fontPath,
DebugMode = debug,
};
Progressing.IsIndeterminate = true;
var ssFt = new SubsetByPyFT();
var ssFt = new SubsetCore();
await ssFt.SubsetAsync(path, fontPath, outputPath, binPath, subsetConfig);
Progressing.IsIndeterminate = false;
await ShowMessageBox("Sucess", "子集化完成,请检查 output 文件夹");
Expand Down
15 changes: 14 additions & 1 deletion AssFontSubset.Console/AssFontSubset.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<PublishAot>True</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="PublishAotCross" Version="1.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.24324.3" />
Expand All @@ -18,4 +18,17 @@

<Import Project="..\build\global.props" />

<Target Name="CopyNativeFiles" AfterTargets="Build"
Condition=" '$(Configuration)' == 'Debug' and $([MSBuild]::IsOSPlatform('Windows')) ">
<Copy SourceFiles="..\native\harfbuzz.dll" DestinationFolder="$(OutputPath)" />
<Copy SourceFiles="..\native\harfbuzz-subset.dll" DestinationFolder="$(OutputPath)" />
</Target>

<ItemGroup>
<DirectPInvoke Include="harfbuzz" />
<NativeLibrary Include="../native/libharfbuzz.a" Condition="$(RuntimeIdentifier.StartsWith('win-x64'))" />
<DirectPInvoke Include="harfbuzz-subset" />
<NativeLibrary Include="../native/libharfbuzz-subset.a" Condition="$(RuntimeIdentifier.StartsWith('win-x64'))" />
</ItemGroup>

</Project>
15 changes: 11 additions & 4 deletions AssFontSubset.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ static async Task<int> Main(string[] args)
{
Description = "子集化后成品所在目录,默认为 ASS 同目录的 output 文件夹"
};
var subsetBackend = new CliOption<SubsetBackend>("--subset-backend")
{
Description = "子集化使用的后端(目前只有 win-x64 支持 HarfBuzzSubset)",
DefaultValueFactory = _ => SubsetBackend.PyFontTools,
};
var binPath = new CliOption<DirectoryInfo>("--bin-path")
{
Description = "指定 pyftsubset 和 ttx 所在目录。若未指定,会使用环境变量中的"
Expand All @@ -36,9 +41,9 @@ static async Task<int> Main(string[] args)
DefaultValueFactory = _ => false,
};

var rootCommand = new CliRootCommand("使用 fonttools 生成 ASS 字幕文件的字体子集,并自动修改字体名称及 ASS 文件中对应的字体名称")
var rootCommand = new CliRootCommand("使用 fonttools 或 harfbuzz-subset 生成 ASS 字幕文件的字体子集,并自动修改字体名称及 ASS 文件中对应的字体名称")
{
path, fontPath, outputPath, binPath, sourceHanEllipsis, debug
path, fontPath, outputPath, subsetBackend, binPath, sourceHanEllipsis, debug
};

rootCommand.SetAction(async (result, _) =>
Expand All @@ -47,6 +52,7 @@ await Subset(
result.GetValue(path)!,
result.GetValue(fontPath),
result.GetValue(outputPath),
result.GetValue(subsetBackend),
result.GetValue(binPath),
result.GetValue(sourceHanEllipsis),
result.GetValue(debug)
Expand Down Expand Up @@ -74,12 +80,13 @@ await Subset(
return exitCode;
}

static async Task Subset(FileInfo[] path, DirectoryInfo? fontPath, DirectoryInfo? outputPath, DirectoryInfo? binPath, bool sourceHanEllipsis, bool debug)
static async Task Subset(FileInfo[] path, DirectoryInfo? fontPath, DirectoryInfo? outputPath, SubsetBackend subsetBackend, DirectoryInfo? binPath, bool sourceHanEllipsis, bool debug)
{
var subsetConfig = new SubsetConfig
{
SourceHanEllipsis = sourceHanEllipsis,
DebugMode = debug,
Backend = subsetBackend,
};
var logLevel = debug ? LogLevel.Debug : LogLevel.Information;

Expand Down Expand Up @@ -115,7 +122,7 @@ static async Task Subset(FileInfo[] path, DirectoryInfo? fontPath, DirectoryInfo
throw new ArgumentException();
}

var ssFt = new SubsetByPyFT(logger);
var ssFt = new SubsetCore(logger);
try
{
await ssFt.SubsetAsync(path, fontPath, outputPath, binPath, subsetConfig);
Expand Down
5 changes: 5 additions & 0 deletions AssFontSubset.Core/AssFontSubset.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- <CheckForOverflowUnderflow>True</CheckForOverflowUnderflow> -->
</PropertyGroup>

Expand All @@ -10,6 +11,10 @@
<PackageReference Include="Mobsub.SubtitleParse" Version="0.4.4-beta3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HarfBuzzBinding\HarfBuzzBinding.csproj" />
</ItemGroup>

<Import Project="..\build\global.props" />

</Project>
17 changes: 17 additions & 0 deletions AssFontSubset.Core/src/FontConstant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,21 @@ public static class FontConstant
};

public const int LanguageIdEnUs = 1033;


// GDI doesn’t seem to use any features (may use vert?), and it has its own logic for handling vertical layout.
// https://learn.microsoft.com/en-us/typography/opentype/spec/features_uz#tag-vrt2
// GDI may according it:
// OpenType font with CFF outlines to be used for vertical writing must have vrt2, otherwise fallback
// OpenType font without CFF outlines use vert map default glyphs to vertical writing glyphs

// https://github.com/libass/libass/pull/702
// libass seems to be trying to use features like vert to solve this problem.
// These are features related to vertical layout but are not enabled: "vchw", "vhal", "vkrn", "vpal", "vrtr".
// https://github.com/libass/libass/blob/6e83137cdbaf4006439d526fef902e123129707b/libass/ass_shaper.c#L147
public static readonly string[] SubsetKeepFeatures = [
"vert", "vrtr",
"vrt2",
"vkna",
];
}
144 changes: 144 additions & 0 deletions AssFontSubset.Core/src/HarfBuzzSubset.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System.Runtime.InteropServices;
using System.Text;
using HarfBuzzBinding;
using Microsoft.Extensions.Logging;
using ZLogger;
using SubsetApis = HarfBuzzBinding.Native.Subset.Apis;
using HBApis = HarfBuzzBinding.Native.Apis;
using System.Diagnostics;
using HarfBuzzBinding.Native.Subset;
using OTFontFile;

namespace AssFontSubset.Core;

public unsafe class HarfBuzzSubset(ILogger? logger) : SubsetToolBase
{
private Version hbssVersion = new Version(Methods.GetHarfBuzzVersion()!);
public SubsetConfig Config;
public Stopwatch? sw;
private long timer;

public override void SubsetFonts(Dictionary<string, List<SubsetFont>> subsetFonts, string outputFolder, out Dictionary<string, string> nameMap)
{
logger?.ZLogInformation($"Start subset font");
logger?.ZLogInformation($"Font subset use harfbuzz-subset {hbssVersion}");
nameMap = [];
logger?.ZLogDebug($"Generate randomly non repeating font names");
var randoms = SubsetFont.GenerateRandomStrings(8, subsetFonts.Keys.Count);

var i = 0;
foreach (var kv in subsetFonts)
{
nameMap[kv.Key] = randoms[i];
foreach (var subsetFont in kv.Value)
{
subsetFont.RandomNewName = randoms[i];
logger?.ZLogInformation($"Start subset {subsetFont.OriginalFontFile.Name}");
timer = 0;
CreateFontSubset(subsetFont, outputFolder);
logger?.ZLogInformation($"Subset font completed, use {timer} ms");
}
}
}

public override void CreateFontSubset(SubsetFont ssf, string outputFolder)
{
if (!Path.Exists(outputFolder))
{
new DirectoryInfo(outputFolder).Create();
}

var outputFileWithoutSuffix = Path.GetFileNameWithoutExtension(ssf.OriginalFontFile.Name);
var outputFile = new StringBuilder($"{outputFileWithoutSuffix}.{ssf.TrackIndex}.{ssf.RandomNewName}");

var originalFontFileSuffix = Path.GetExtension(ssf.OriginalFontFile.Name).AsSpan();
var outFileWithoutSuffix = outputFile.ToString();
outputFile.Append(originalFontFileSuffix[..3]);
switch (originalFontFileSuffix[^1])
{
case 'c':
outputFile.Append('f');
break;
case 'C':
outputFile.Append('F');
break;
default:
outputFile.Append(originalFontFileSuffix[^1]);
break;
}

var outputFileName = Path.Combine(outputFolder, outputFile.ToString());

ssf.Preprocessing();
var modifyIds = GetModifyNameIds(ssf.OriginalFontFile.FullName, ssf.TrackIndex);
if (Config.DebugMode)
{
ssf.CharactersFile = Path.Combine(outputFolder, $"{outFileWithoutSuffix}.txt");
ssf.WriteRunesToUtf8File();
}

sw ??= new Stopwatch();
sw.Start();

_ = Methods.TryGetFontFace(ssf.OriginalFontFile.FullName, ssf.TrackIndex, out var facePtr);
facePtr = SubsetApis.hb_subset_preprocess(facePtr);

var input = SubsetApis.hb_subset_input_create_or_fail();

var unicodes = SubsetApis.hb_subset_input_unicode_set(input);
foreach (var rune in ssf.Runes)
{
HBApis.hb_set_add(unicodes, (uint)rune.Value);
}

var features = SubsetApis.hb_subset_input_set(input, hb_subset_sets_t.HB_SUBSET_SETS_LAYOUT_FEATURE_TAG);
HBApis.hb_set_clear(features);
foreach (var feature in FontConstant.SubsetKeepFeatures)
{
HBApis.hb_set_add(features, HBApis.hb_tag_from_string((sbyte*)Marshal.StringToHGlobalAnsi(feature), -1));
}

Methods.RenameFontname(input,
(sbyte*)Marshal.StringToHGlobalAnsi($"Processed by AssFontSubset v{System.Reflection.Assembly.GetEntryAssembly()!.GetName().Version}; harfbuzz-subset {hbssVersion}"),
(sbyte*)Marshal.StringToHGlobalAnsi(ssf.RandomNewName),
modifyIds);

var faceNewPtr = SubsetApis.hb_subset_or_fail(facePtr, input);

var blobPtr = HBApis.hb_face_reference_blob(faceNewPtr);
Methods.WriteFontFile(blobPtr, outputFileName);

sw.Stop();
timer += sw.ElapsedMilliseconds;
sw.Reset();

SubsetApis.hb_subset_input_destroy(input);
HBApis.hb_face_destroy(faceNewPtr);
HBApis.hb_face_destroy(facePtr);
}

private static OpenTypeNameId[] GetModifyNameIds(string fontFileName, uint index)
{
List<OpenTypeNameId> ids = [];
var otf = new OTFile();
otf.open(fontFileName);
var font = otf.GetFont(index);
var nameTable = (Table_name)font!.GetTable("name")!;
for (uint i = 0; i < nameTable.NumberNameRecords; i++)
{
var nameRecord = nameTable.GetNameRecord(i);
if (nameRecord!.NameID is 0 or 1 or 3 or 4 or 6)
{
ids.Add(new OpenTypeNameId
{
NameId = nameRecord.NameID,
PlatformId = nameRecord.PlatformID,
LanguageId = nameRecord.LanguageID,
EncodingId = nameRecord.EncodingID,
});
}
}

return ids.ToArray();
}
}
29 changes: 4 additions & 25 deletions AssFontSubset.Core/src/PyFontTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,7 @@

namespace AssFontSubset.Core;

public struct SubsetConfig
{
public bool SourceHanEllipsis;
public bool DebugMode;
}

public class PyFontTools(string pyftsubset, string ttx, ILogger? logger)
public class PyFontTools(string pyftsubset, string ttx, ILogger? logger) : SubsetToolBase
{
private Version pyFtVersion = GetFontToolsVersion(ttx);

Expand Down Expand Up @@ -42,7 +36,7 @@ public void SubsetFonts(List<SubsetFont> subsetFonts, string outputFolder)
}
}

public void SubsetFonts(Dictionary<string, List<SubsetFont>> subsetFonts, string outputFolder, out Dictionary<string, string> nameMap)
public override void SubsetFonts(Dictionary<string, List<SubsetFont>> subsetFonts, string outputFolder, out Dictionary<string, string> nameMap)
{
logger?.ZLogInformation($"Start subset font");
logger?.ZLogInformation($"Font subset use pyFontTools {pyFtVersion}");
Expand Down Expand Up @@ -74,7 +68,7 @@ public void SubsetFonts(Dictionary<string, List<SubsetFont>> subsetFonts, string
}
}

public void CreateFontSubset(SubsetFont ssf, string outputFolder)
public override void CreateFontSubset(SubsetFont ssf, string outputFolder)
{
if (!Path.Exists(outputFolder))
{
Expand Down Expand Up @@ -255,29 +249,14 @@ private ProcessStartInfo GetSubsetCmd(SubsetFont ssf)
{
var startInfo = GetSimpleCmd(pyftsubset);

// GDI doesn’t seem to use any features (may use vert?), and it has its own logic for handling vertical layout.
// https://learn.microsoft.com/en-us/typography/opentype/spec/features_uz#tag-vrt2
// GDI may according it:
// OpenType font with CFF outlines to be used for vertical writing must have vrt2, otherwise fallback
// OpenType font without CFF outlines use vert map default glyphs to vertical writing glyphs

// https://github.com/libass/libass/pull/702
// libass seems to be trying to use features like vert to solve this problem.
// These are features related to vertical layout but are not enabled: "vchw", "vhal", "vkrn", "vpal", "vrtr".
// https://github.com/libass/libass/blob/6e83137cdbaf4006439d526fef902e123129707b/libass/ass_shaper.c#L147
string[] enableFeatures = [
"vert", "vrtr",
"vrt2",
"vkna",
];
string[] argus = [
ssf.OriginalFontFile.FullName,
$"--text-file={ssf.CharactersFile!}",
$"--output-file={ssf.SubsetFontFileTemp!}",
"--name-languages=*",
$"--font-number={ssf.TrackIndex}",
// "--no-layout-closure",
$"--layout-features={string.Join(",", enableFeatures)}",
$"--layout-features={string.Join(",", FontConstant.SubsetKeepFeatures)}",
// "--layout-features=*",
];
foreach (var arg in argus)
Expand Down
14 changes: 14 additions & 0 deletions AssFontSubset.Core/src/SubsetConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace AssFontSubset.Core;

public struct SubsetConfig
{
public bool SourceHanEllipsis;
public bool DebugMode;
public SubsetBackend Backend;
}

public enum SubsetBackend
{
PyFontTools = 1,
HarfBuzzSubset = 2,
}
Loading

0 comments on commit f0579b5

Please sign in to comment.