Crunchy-Downloader/CRD/Utils/Muxing/FontsManager.cs
Elwador 985fd9c00f Added **tray icon** [#393](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/393).
Added **ability to switch between account profiles** [#372](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/372).
Added option to **execute a file when the download queue finishes** [#392](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/392).
Added **auto history refresh / auto add to queue** [#394](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/394).
Changed **font loading** to also include fonts from the local fonts folder that are not available on Crunchyroll [#371](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/371).
Updated packages to latest versions
Fixed **history not being saved** after it was updated via the calendar
Fixed **Downloaded toggle in history** being slow for large seasons
2026-03-04 18:17:28 +01:00

456 lines
No EOL
16 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using CRD.Utils.Files;
using CRD.Utils.Structs;
using CRD.Views;
using SixLabors.Fonts;
namespace CRD.Utils.Muxing;
public class FontsManager{
#region Singelton
private static readonly Lock Padlock = new Lock();
public static FontsManager Instance{
get{
if (field == null){
lock (Padlock){
if (field == null){
field = new FontsManager();
}
}
}
return field;
}
}
#endregion
public Dictionary<string, string> Fonts{ get; private set; } = new(StringComparer.OrdinalIgnoreCase){
{ "Adobe Arabic", "AdobeArabic-Bold.otf" },
{ "Andale Mono", "andalemo.ttf" },
{ "Arial", "arial.ttf" },
{ "Arial Black", "ariblk.ttf" },
{ "Arial Bold", "arialbd.ttf" },
{ "Arial Bold Italic", "arialbi.ttf" },
{ "Arial Italic", "ariali.ttf" },
{ "Arial Unicode MS", "arialuni.ttf" },
{ "Comic Sans MS", "comic.ttf" },
{ "Comic Sans MS Bold", "comicbd.ttf" },
{ "Courier New", "cour.ttf" },
{ "Courier New Bold", "courbd.ttf" },
{ "Courier New Bold Italic", "courbi.ttf" },
{ "Courier New Italic", "couri.ttf" },
{ "DejaVu LGC Sans Mono", "DejaVuLGCSansMono.ttf" },
{ "DejaVu LGC Sans Mono Bold", "DejaVuLGCSansMono-Bold.ttf" },
{ "DejaVu LGC Sans Mono Bold Oblique", "DejaVuLGCSansMono-BoldOblique.ttf" },
{ "DejaVu LGC Sans Mono Oblique", "DejaVuLGCSansMono-Oblique.ttf" },
{ "DejaVu Sans", "DejaVuSans.ttf" },
{ "DejaVu Sans Bold", "DejaVuSans-Bold.ttf" },
{ "DejaVu Sans Bold Oblique", "DejaVuSans-BoldOblique.ttf" },
{ "DejaVu Sans Condensed", "DejaVuSansCondensed.ttf" },
{ "DejaVu Sans Condensed Bold", "DejaVuSansCondensed-Bold.ttf" },
{ "DejaVu Sans Condensed Bold Oblique", "DejaVuSansCondensed-BoldOblique.ttf" },
{ "DejaVu Sans Condensed Oblique", "DejaVuSansCondensed-Oblique.ttf" },
{ "DejaVu Sans ExtraLight", "DejaVuSans-ExtraLight.ttf" },
{ "DejaVu Sans Mono", "DejaVuSansMono.ttf" },
{ "DejaVu Sans Mono Bold", "DejaVuSansMono-Bold.ttf" },
{ "DejaVu Sans Mono Bold Oblique", "DejaVuSansMono-BoldOblique.ttf" },
{ "DejaVu Sans Mono Oblique", "DejaVuSansMono-Oblique.ttf" },
{ "DejaVu Sans Oblique", "DejaVuSans-Oblique.ttf" },
{ "Gautami", "gautami.ttf" },
{ "Georgia", "georgia.ttf" },
{ "Georgia Bold", "georgiab.ttf" },
{ "Georgia Bold Italic", "georgiaz.ttf" },
{ "Georgia Italic", "georgiai.ttf" },
{ "Impact", "impact.ttf" },
{ "Mangal", "MANGAL.woff2" },
{ "Meera Inimai", "MeeraInimai-Regular.ttf" },
{ "Noto Sans Tamil", "NotoSansTamilVariable.ttf" },
{ "Noto Sans Telugu", "NotoSansTeluguVariable.ttf" },
{ "Noto Sans Thai", "NotoSansThai.ttf" },
{ "Rubik", "Rubik-Regular.ttf" },
{ "Rubik Black", "Rubik-Black.ttf" },
{ "Rubik Black Italic", "Rubik-BlackItalic.ttf" },
{ "Rubik Bold", "Rubik-Bold.ttf" },
{ "Rubik Bold Italic", "Rubik-BoldItalic.ttf" },
{ "Rubik Italic", "Rubik-Italic.ttf" },
{ "Rubik Light", "Rubik-Light.ttf" },
{ "Rubik Light Italic", "Rubik-LightItalic.ttf" },
{ "Rubik Medium", "Rubik-Medium.ttf" },
{ "Rubik Medium Italic", "Rubik-MediumItalic.ttf" },
{ "Tahoma", "tahoma.ttf" },
{ "Times New Roman", "times.ttf" },
{ "Times New Roman Bold", "timesbd.ttf" },
{ "Times New Roman Bold Italic", "timesbi.ttf" },
{ "Times New Roman Italic", "timesi.ttf" },
{ "Trebuchet MS", "trebuc.ttf" },
{ "Trebuchet MS Bold", "trebucbd.ttf" },
{ "Trebuchet MS Bold Italic", "trebucbi.ttf" },
{ "Trebuchet MS Italic", "trebucit.ttf" },
{ "Verdana", "verdana.ttf" },
{ "Verdana Bold", "verdanab.ttf" },
{ "Verdana Bold Italic", "verdanaz.ttf" },
{ "Verdana Italic", "verdanai.ttf" },
{ "Vrinda", "vrinda.ttf" },
{ "Vrinda Bold", "vrindab.ttf" },
{ "Webdings", "webdings.ttf" }
};
private string root = "https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fonts/";
private readonly FontIndex index = new();
private void EnsureIndex(string fontsDir){
index.Rebuild(fontsDir);
}
public async Task GetFontsAsync(){
Console.WriteLine("Downloading fonts...");
var fonts = Fonts.Values.ToList();
foreach (var font in fonts){
var fontLoc = Path.Combine(CfgManager.PathFONTS_DIR, font);
if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length != 0){
continue;
}
var fontFolder = Path.GetDirectoryName(fontLoc);
if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length == 0)
File.Delete(fontLoc);
try{
if (!Directory.Exists(fontFolder))
Directory.CreateDirectory(fontFolder!);
} catch (Exception e){
Console.WriteLine($"Failed to create directory: {e.Message}");
}
var fontUrl = root + font;
var httpClient = HttpClientReq.Instance.GetHttpClient();
try{
var response = await httpClient.GetAsync(fontUrl);
if (response.IsSuccessStatusCode){
var fontData = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync(fontLoc, fontData);
Console.WriteLine($"Downloaded: {font}");
} else{
Console.Error.WriteLine($"Failed to download: {font}");
}
} catch (Exception e){
Console.Error.WriteLine($"Error downloading {font}: {e.Message}");
}
}
Console.WriteLine("All required fonts downloaded!");
}
public static List<string> ExtractFontsFromAss(string ass){
if (string.IsNullOrWhiteSpace(ass))
return new List<string>();
ass = ass.Replace("\r", "");
var lines = ass.Split('\n');
var fonts = new List<string>();
foreach (var line in lines){
if (line.StartsWith("Style: ", StringComparison.OrdinalIgnoreCase)){
var parts = line.Substring(7).Split(',');
if (parts.Length > 1){
var fontName = parts[1].Trim();
fonts.Add(NormalizeFontKey(fontName));
}
}
}
var fontMatches = Regex.Matches(ass, @"\\fn([^\\}]+)");
foreach (Match match in fontMatches){
if (match.Groups.Count > 1){
var fontName = match.Groups[1].Value.Trim();
fonts.Add(NormalizeFontKey(fontName));
}
}
return fonts
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
public Dictionary<string, string> GetDictFromKeyList(List<string> keysList, bool keepUnknown = true){
Dictionary<string, string> filteredDictionary = new(StringComparer.OrdinalIgnoreCase);
foreach (string key in keysList){
var k = NormalizeFontKey(key);
if (Fonts.TryGetValue(k, out var fontFile)){
filteredDictionary[k] = fontFile;
} else if (keepUnknown){
filteredDictionary[k] = k;
}
}
return filteredDictionary;
}
public static string GetFontMimeType(string fontFileOrPath){
var ext = Path.GetExtension(fontFileOrPath);
if (ext.Equals(".otf", StringComparison.OrdinalIgnoreCase))
return "application/vnd.ms-opentype";
if (ext.Equals(".ttf", StringComparison.OrdinalIgnoreCase))
return "application/x-truetype-font";
if (ext.Equals(".ttc", StringComparison.OrdinalIgnoreCase) || ext.Equals(".otc", StringComparison.OrdinalIgnoreCase))
return "application/x-truetype-font";
if (ext.Equals(".woff", StringComparison.OrdinalIgnoreCase))
return "font/woff";
if (ext.Equals(".woff2", StringComparison.OrdinalIgnoreCase))
return "font/woff2";
return "application/octet-stream";
}
public List<ParsedFont> MakeFontsList(string fontsDir, List<SubtitleFonts> subs){
EnsureIndex(fontsDir);
var required = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var subsLocales = new List<string>();
var fontsList = new List<ParsedFont>();
var missing = new List<string>();
foreach (var s in subs){
subsLocales.Add(s.Language.Locale);
foreach (var kv in s.Fonts)
required.Add(NormalizeFontKey(kv.Key));
}
if (subsLocales.Count > 0)
Console.WriteLine("\nSubtitles: {0} (Total: {1})", string.Join(", ", subsLocales), subsLocales.Count);
if (required.Count > 0)
Console.WriteLine("Required fonts: {0} (Total: {1})", string.Join(", ", required), required.Count);
foreach (var requested in required){
if (TryResolveFontPath(requested, fontsDir, out var resolvedPath, out var exact)){
if (!File.Exists(resolvedPath) || new FileInfo(resolvedPath).Length == 0){
missing.Add(requested);
continue;
}
var attachName = MakeUniqueAttachmentName(resolvedPath, fontsList);
fontsList.Add(new ParsedFont{
Name = attachName,
Path = resolvedPath,
Mime = GetFontMimeType(resolvedPath)
});
if (!exact) Console.WriteLine($"Soft-resolved '{requested}' -> '{Path.GetFileName(resolvedPath)}'");
} else{
missing.Add(requested);
}
}
if (missing.Count > 0)
MainWindow.Instance.ShowError($"Missing Fonts:\n{string.Join(", ", missing)}");
return fontsList;
}
private bool TryResolveFontPath(string requestedName, string fontsDir, out string resolvedPath, out bool isExactMatch){
resolvedPath = string.Empty;
isExactMatch = true;
var req = NormalizeFontKey(requestedName);
if (index.TryResolve(req, out resolvedPath))
return true;
if (Fonts.TryGetValue(req, out var crFile)){
var p = Path.Combine(fontsDir, crFile);
if (File.Exists(p)){
resolvedPath = p;
return true;
}
}
var family = StripStyleSuffix(req);
if (!family.Equals(req, StringComparison.OrdinalIgnoreCase)){
isExactMatch = false;
if (index.TryResolve(family, out resolvedPath))
return true;
if (Fonts.TryGetValue(family, out var crFamilyFile)){
var p = Path.Combine(fontsDir, crFamilyFile);
if (File.Exists(p)){
resolvedPath = p;
return true;
}
}
}
return false;
}
private static string StripStyleSuffix(string name){
var n = name;
n = Regex.Replace(n, @"\s+(Bold\s+Italic|Bold\s+Oblique|Black\s+Italic|Black|Bold|Italic|Oblique|Regular)$",
"", RegexOptions.IgnoreCase).Trim();
return n;
}
public static string NormalizeFontKey(string s){
if (string.IsNullOrWhiteSpace(s))
return string.Empty;
s = s.Trim().Trim('"');
if (s.StartsWith("@"))
s = s.Substring(1);
s = Regex.Replace(s, @"(?<=[a-z])([A-Z])", " $1");
s = s.Replace('_', ' ').Replace('-', ' ');
s = Regex.Replace(s, @"\s+", " ").Trim();
s = Regex.Replace(s, @"\s+Regular$", "", RegexOptions.IgnoreCase);
return s;
}
private static string MakeUniqueAttachmentName(string path, List<ParsedFont> existing){
var baseName = Path.GetFileName(path);
if (existing.All(e => !baseName.Equals(e.Name, StringComparison.OrdinalIgnoreCase)))
return baseName;
var hash = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(path)))
.Substring(0, 8)
.ToLowerInvariant();
return $"{hash}-{baseName}";
}
private sealed class FontIndex{
private readonly Dictionary<string, Candidate> map = new(StringComparer.OrdinalIgnoreCase);
public void Rebuild(string fontsDir){
map.Clear();
if (!Directory.Exists(fontsDir)) return;
foreach (var path in Directory.EnumerateFiles(fontsDir, "*.*", SearchOption.AllDirectories)){
var ext = Path.GetExtension(path).ToLowerInvariant();
if (ext is not (".ttf" or ".otf" or ".ttc" or ".otc" or ".woff" or ".woff2"))
continue;
foreach (var desc in LoadDescriptions(path)){
foreach (var alias in BuildAliases(desc)){
Add(alias, path);
}
}
}
}
public bool TryResolve(string fontName, out string path){
path = string.Empty;
if (string.IsNullOrWhiteSpace(fontName)) return false;
var key = NormalizeFontKey(fontName);
if (map.TryGetValue(fontName, out var c1)){
path = c1.Path;
return true;
}
if (map.TryGetValue(key, out var c2)){
path = c2.Path;
return true;
}
return false;
}
private void Add(string alias, string path){
if (string.IsNullOrWhiteSpace(alias)) return;
var a1 = alias.Trim();
var a2 = NormalizeFontKey(a1);
Upsert(a1, path);
Upsert(a2, path);
}
private void Upsert(string key, string path){
if (string.IsNullOrWhiteSpace(key)) return;
var cand = new Candidate(path, GetScore(path));
if (map.TryGetValue(key, out var existing)){
if (cand.Score > existing.Score)
map[key] = cand;
} else{
map[key] = cand;
}
}
private static int GetScore(string path){
var ext = Path.GetExtension(path).ToLowerInvariant();
return ext switch{
".ttf" => 100,
".otf" => 95,
".ttc" => 90,
".otc" => 85,
".woff" => 40,
".woff2" => 35,
_ => 0
};
}
private static IEnumerable<FontDescription> LoadDescriptions(string fontPath){
var ext = Path.GetExtension(fontPath).ToLowerInvariant();
if (ext is ".ttc" or ".otc")
return FontDescription.LoadFontCollectionDescriptions(fontPath);
return new[]{ FontDescription.LoadDescription(fontPath) };
}
private static IEnumerable<string> BuildAliases(FontDescription d){
var family = d.FontFamilyInvariantCulture.Trim();
var sub = d.FontSubFamilyNameInvariantCulture.Trim(); // Regular/Bold/Italic
var full = d.FontNameInvariantCulture.Trim(); // "Family Subfamily"
if (!string.IsNullOrWhiteSpace(family)) yield return family;
if (!string.IsNullOrWhiteSpace(full)) yield return full;
if (!string.IsNullOrWhiteSpace(family) &&
!string.IsNullOrWhiteSpace(sub) &&
!sub.Equals("Regular", StringComparison.OrdinalIgnoreCase)){
yield return $"{family} {sub}";
}
}
private readonly record struct Candidate(string Path, int Score);
}
}
public class SubtitleFonts{
public LanguageItem Language{ get; set; } = new();
public Dictionary<string, string> Fonts{ get; set; } = new();
}