Files
DCIT/DCIT.Core/Services/FontValidationService.cs

909 lines
33 KiB
C#
Raw Normal View History

2026-07-13 10:04:36 +08:00
using System;
using System.Collections.Generic;
using System.Drawing.Text;
using System.IO;
using System.Linq;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using W = DocumentFormat.OpenXml.Wordprocessing;
namespace DCIT.Core.Services
{
public sealed class FontValidationService : IFontValidationService
{
private const string WordprocessingNamespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
private const string DefaultEastAsiaFallbackFont = "宋体";
private const string DefaultLatinFallbackFont = "Times New Roman";
private static readonly string[] ThemeFontTokens =
{
"majorAscii",
"majorHAnsi",
"majorEastAsia",
"majorBidi",
"minorAscii",
"minorHAnsi",
"minorEastAsia",
"minorBidi",
};
private static readonly string[][] KnownFontAliasGroups =
{
new[] { "宋体", "SimSun" },
new[] { "新宋体", "NSimSun" },
new[] { "黑体", "SimHei" },
new[] { "仿宋", "FangSong" },
new[] { "楷体", "KaiTi" },
new[] { "微软雅黑", "Microsoft YaHei" },
new[] { "微软雅黑 Light", "Microsoft YaHei Light" },
new[] { "微软雅黑 Bold", "Microsoft YaHei Bold" },
new[] { "等线", "DengXian" },
new[] { "等线 Light", "DengXian Light" },
new[] { "等线 Bold", "DengXian Bold" },
new[] { "华文宋体", "STSong" },
new[] { "华文黑体", "STHeiti" },
new[] { "华文中宋", "STZhongsong" },
new[] { "华文仿宋", "STFangsong" },
new[] { "华文楷体", "STKaiti" },
new[] { "幼圆", "YouYuan" },
new[] { "隶书", "LiSu" },
};
private static readonly Lazy<Dictionary<string, HashSet<string>>> KnownFontAliases =
new Lazy<Dictionary<string, HashSet<string>>>(BuildKnownFontAliases);
private readonly Lazy<HashSet<string>> _installedFonts;
public FontValidationService()
: this(null)
{
}
public FontValidationService(IEnumerable<string> installedFonts)
{
_installedFonts = new Lazy<HashSet<string>>(
delegate
{
return installedFonts == null
? LoadInstalledFonts()
: new HashSet<string>(
installedFonts
.Select(NormalizeFontName)
.Where(name => !string.IsNullOrWhiteSpace(name)),
StringComparer.OrdinalIgnoreCase);
});
}
public FontValidationResult Validate(string docxPath)
{
if (string.IsNullOrWhiteSpace(docxPath))
{
throw new ArgumentException("docxPath");
}
if (!File.Exists(docxPath))
{
throw new FileNotFoundException("未找到待校验的文档。", docxPath);
}
using (var document = WordprocessingDocument.Open(docxPath, false))
{
var themeFonts = BuildThemeFontMap(document);
var styleIndex = BuildStyleIndex(document);
var defaultFonts = ReadDocumentDefaultFonts(document, themeFonts);
var documentFontAliases = BuildDocumentFontAliasMap(document);
var usedFonts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var root in EnumerateRelevantRoots(document))
{
CollectWordRunFonts(root, styleIndex, defaultFonts, themeFonts, usedFonts);
CollectDrawingFonts(root, usedFonts);
}
var missingFonts = usedFonts
.Select(NormalizeFontName)
.Where(name => !string.IsNullOrWhiteSpace(name))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Where(name => !IsFontAvailable(name, documentFontAliases))
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
.ToList();
return new FontValidationResult(
usedFonts.OrderBy(name => name, StringComparer.OrdinalIgnoreCase).ToList(),
missingFonts);
}
}
public FontValidationResult EnsureFontsAvailable(string docxPath)
{
var result = Validate(docxPath);
if (result.MissingFonts.Count == 0)
{
return result;
}
ApplyMissingFontFallbacks(docxPath, result.MissingFonts);
return result;
}
private static void ApplyMissingFontFallbacks(string docxPath, IEnumerable<string> missingFonts)
{
var missingFontSet = new HashSet<string>(
(missingFonts ?? Array.Empty<string>())
.Select(NormalizeFontName)
.Where(name => !string.IsNullOrWhiteSpace(name)),
StringComparer.OrdinalIgnoreCase);
if (missingFontSet.Count == 0)
{
return;
}
using (var document = WordprocessingDocument.Open(docxPath, true))
{
var themeFonts = BuildThemeFontMap(document);
foreach (var root in EnumerateFontFallbackRoots(document))
{
var changed = false;
foreach (var runFonts in root.Descendants<W.RunFonts>())
{
changed |= ApplyWordRunFontFallbacks(runFonts, missingFontSet, themeFonts);
}
foreach (var element in root.Descendants())
{
changed |= ApplyDrawingFontFallback(element, missingFontSet);
}
if (changed)
{
root.Save();
}
}
}
}
private static IEnumerable<OpenXmlPartRootElement> EnumerateFontFallbackRoots(WordprocessingDocument document)
{
var main = document.MainDocumentPart;
if (main == null)
{
yield break;
}
if (main.Document != null)
{
yield return main.Document;
}
var queue = new Queue<OpenXmlPartContainer>();
var visited = new HashSet<OpenXmlPart>();
queue.Enqueue(main);
while (queue.Count > 0)
{
var container = queue.Dequeue();
foreach (var pair in container.Parts)
{
var part = pair.OpenXmlPart;
if (part == null || !visited.Add(part))
{
continue;
}
if (part.RootElement != null)
{
yield return part.RootElement;
}
queue.Enqueue(part);
}
}
}
private static bool ApplyWordRunFontFallbacks(
W.RunFonts runFonts,
ISet<string> missingFonts,
IDictionary<string, string> themeFonts)
{
var changed = false;
changed |= ApplyWordFontFallback(
runFonts,
"ascii",
new[] { "asciiTheme" },
DefaultLatinFallbackFont,
missingFonts,
themeFonts);
changed |= ApplyWordFontFallback(
runFonts,
"hAnsi",
new[] { "hAnsiTheme" },
DefaultLatinFallbackFont,
missingFonts,
themeFonts);
changed |= ApplyWordFontFallback(
runFonts,
"eastAsia",
new[] { "eastAsiaTheme" },
DefaultEastAsiaFallbackFont,
missingFonts,
themeFonts);
changed |= ApplyWordFontFallback(
runFonts,
"cs",
new[] { "csTheme", "cstheme" },
DefaultLatinFallbackFont,
missingFonts,
themeFonts);
return changed;
}
private static bool ApplyWordFontFallback(
OpenXmlElement element,
string directLocalName,
IEnumerable<string> themeLocalNames,
string fallbackFont,
ISet<string> missingFonts,
IDictionary<string, string> themeFonts)
{
var directFont = NormalizeFontName(GetAttributeValue(element, directLocalName));
var resolvedFont = directFont;
if (string.IsNullOrWhiteSpace(resolvedFont))
{
foreach (var themeLocalName in themeLocalNames)
{
resolvedFont = ResolveFontName(null, GetAttributeValue(element, themeLocalName), themeFonts);
if (!string.IsNullOrWhiteSpace(resolvedFont))
{
break;
}
}
}
if (string.IsNullOrWhiteSpace(resolvedFont) || !missingFonts.Contains(resolvedFont))
{
return false;
}
var changed = false;
if (!string.Equals(directFont, fallbackFont, StringComparison.OrdinalIgnoreCase))
{
SetWordprocessingAttribute(element, directLocalName, fallbackFont);
changed = true;
}
changed |= RemoveAttributesByLocalName(element, themeLocalNames);
return changed;
}
private static bool ApplyDrawingFontFallback(OpenXmlElement element, ISet<string> missingFonts)
{
if (!(string.Equals(element.LocalName, "latin", StringComparison.OrdinalIgnoreCase)
|| string.Equals(element.LocalName, "ea", StringComparison.OrdinalIgnoreCase)
|| string.Equals(element.LocalName, "cs", StringComparison.OrdinalIgnoreCase)
|| string.Equals(element.LocalName, "font", StringComparison.OrdinalIgnoreCase)))
{
return false;
}
var typeface = NormalizeFontName(GetAttributeValue(element, "typeface"));
if (string.IsNullOrWhiteSpace(typeface) || !missingFonts.Contains(typeface))
{
return false;
}
var fallbackFont = ResolveDrawingFallbackFont(element);
if (string.Equals(typeface, fallbackFont, StringComparison.OrdinalIgnoreCase))
{
return false;
}
element.SetAttribute(new OpenXmlAttribute(string.Empty, "typeface", string.Empty, fallbackFont));
return true;
}
private static string ResolveDrawingFallbackFont(OpenXmlElement element)
{
if (string.Equals(element.LocalName, "ea", StringComparison.OrdinalIgnoreCase))
{
return DefaultEastAsiaFallbackFont;
}
if (string.Equals(element.LocalName, "font", StringComparison.OrdinalIgnoreCase)
&& IsEastAsiaScript(GetAttributeValue(element, "script")))
{
return DefaultEastAsiaFallbackFont;
}
return DefaultLatinFallbackFont;
}
private static bool IsEastAsiaScript(string script)
{
var normalized = NormalizeFontName(script);
if (string.IsNullOrWhiteSpace(normalized))
{
return false;
}
return normalized.IndexOf("Hans", StringComparison.OrdinalIgnoreCase) >= 0
|| normalized.IndexOf("Hant", StringComparison.OrdinalIgnoreCase) >= 0
|| normalized.IndexOf("Jpan", StringComparison.OrdinalIgnoreCase) >= 0
|| normalized.IndexOf("Hang", StringComparison.OrdinalIgnoreCase) >= 0
|| normalized.IndexOf("Kore", StringComparison.OrdinalIgnoreCase) >= 0
|| normalized.IndexOf("Bopo", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static void SetWordprocessingAttribute(OpenXmlElement element, string localName, string value)
{
element.SetAttribute(new OpenXmlAttribute("w", localName, WordprocessingNamespace, value));
}
private static bool RemoveAttributesByLocalName(OpenXmlElement element, IEnumerable<string> localNames)
{
var names = new HashSet<string>(
(localNames ?? Array.Empty<string>()).Where(name => !string.IsNullOrWhiteSpace(name)),
StringComparer.OrdinalIgnoreCase);
if (names.Count == 0)
{
return false;
}
var changed = false;
foreach (var attribute in element.GetAttributes().Where(attribute => names.Contains(attribute.LocalName)).ToList())
{
element.RemoveAttribute(attribute.LocalName, attribute.NamespaceUri);
changed = true;
}
return changed;
}
private static HashSet<string> LoadInstalledFonts()
{
var collection = new InstalledFontCollection();
return new HashSet<string>(
collection.Families
.Select(item => NormalizeFontName(item.Name))
.Where(name => !string.IsNullOrWhiteSpace(name)),
StringComparer.OrdinalIgnoreCase);
}
private static Dictionary<string, HashSet<string>> BuildDocumentFontAliasMap(WordprocessingDocument document)
{
var result = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
var fontTable = document.MainDocumentPart == null || document.MainDocumentPart.FontTablePart == null
? null
: document.MainDocumentPart.FontTablePart.Fonts;
if (fontTable == null)
{
return result;
}
foreach (var font in fontTable.Elements<W.Font>())
{
var primary = NormalizeFontName(font.Name == null ? null : font.Name.Value);
var alternate = NormalizeFontName(font.AltName == null || font.AltName.Val == null ? null : font.AltName.Val.Value);
AddAliasPair(result, primary, alternate);
}
return result;
}
private static IEnumerable<OpenXmlPartRootElement> EnumerateRelevantRoots(WordprocessingDocument document)
{
var main = document.MainDocumentPart;
if (main == null)
{
yield break;
}
if (main.Document != null)
{
yield return main.Document;
}
var queue = new Queue<OpenXmlPartContainer>();
var visited = new HashSet<OpenXmlPart>();
queue.Enqueue(main);
while (queue.Count > 0)
{
var container = queue.Dequeue();
foreach (var pair in container.Parts)
{
var part = pair.OpenXmlPart;
if (part == null || !visited.Add(part))
{
continue;
}
if (part.RootElement != null && IsRelevantPart(part))
{
yield return part.RootElement;
}
queue.Enqueue(part);
}
}
}
private static bool IsRelevantPart(OpenXmlPart part)
{
return part is HeaderPart
|| part is FooterPart
|| part is WordprocessingCommentsPart
|| part is FootnotesPart
|| part is EndnotesPart
|| part.GetType().Name.IndexOf("ChartPart", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static Dictionary<string, StyleFontSet> BuildStyleIndex(WordprocessingDocument document)
{
var result = new Dictionary<string, StyleFontSet>(StringComparer.OrdinalIgnoreCase);
var styles = document.MainDocumentPart == null || document.MainDocumentPart.StyleDefinitionsPart == null
? null
: document.MainDocumentPart.StyleDefinitionsPart.Styles;
if (styles == null)
{
return result;
}
foreach (var style in styles.Elements<W.Style>())
{
var styleId = style.StyleId == null ? null : style.StyleId.Value;
if (string.IsNullOrWhiteSpace(styleId))
{
continue;
}
result[styleId] = new StyleFontSet
{
StyleId = styleId,
BasedOnStyleId = style.BasedOn == null ? null : style.BasedOn.Val == null ? null : style.BasedOn.Val.Value,
Fonts = ExtractWordFontSet(style.StyleRunProperties == null ? null : style.StyleRunProperties.RunFonts, null),
};
}
return result;
}
private static FontReferenceSet ReadDocumentDefaultFonts(WordprocessingDocument document, IDictionary<string, string> themeFonts)
{
var styles = document.MainDocumentPart == null || document.MainDocumentPart.StyleDefinitionsPart == null
? null
: document.MainDocumentPart.StyleDefinitionsPart.Styles;
var docDefaults = styles == null ? null : styles.DocDefaults;
var runDefaults = docDefaults == null ? null : docDefaults.RunPropertiesDefault;
var baseStyle = runDefaults == null ? null : runDefaults.RunPropertiesBaseStyle;
return ExtractWordFontSet(baseStyle == null ? null : baseStyle.RunFonts, themeFonts);
}
private static IDictionary<string, string> BuildThemeFontMap(WordprocessingDocument document)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var themePart = document.MainDocumentPart == null ? null : document.MainDocumentPart.ThemePart;
var theme = themePart == null ? null : themePart.Theme;
if (theme == null)
{
return result;
}
var majorFont = theme.Descendants().FirstOrDefault(item => string.Equals(item.LocalName, "majorFont", StringComparison.OrdinalIgnoreCase));
var minorFont = theme.Descendants().FirstOrDefault(item => string.Equals(item.LocalName, "minorFont", StringComparison.OrdinalIgnoreCase));
AddThemeSlot(result, "majorAscii", majorFont, "latin");
AddThemeSlot(result, "majorHAnsi", majorFont, "latin");
AddThemeSlot(result, "majorEastAsia", majorFont, "ea");
AddThemeSlot(result, "majorBidi", majorFont, "cs");
AddThemeSlot(result, "minorAscii", minorFont, "latin");
AddThemeSlot(result, "minorHAnsi", minorFont, "latin");
AddThemeSlot(result, "minorEastAsia", minorFont, "ea");
AddThemeSlot(result, "minorBidi", minorFont, "cs");
return result;
}
private static void AddThemeSlot(IDictionary<string, string> themeFonts, string token, OpenXmlElement schemeRoot, string localName)
{
if (schemeRoot == null)
{
return;
}
var element = schemeRoot.Elements().FirstOrDefault(item => string.Equals(item.LocalName, localName, StringComparison.OrdinalIgnoreCase));
if (element == null)
{
return;
}
var typeface = GetAttributeValue(element, "typeface");
var normalized = NormalizeFontName(typeface);
if (!string.IsNullOrWhiteSpace(normalized))
{
themeFonts[token] = normalized;
}
}
private static void CollectWordRunFonts(
OpenXmlElement root,
IDictionary<string, StyleFontSet> styleIndex,
FontReferenceSet defaultFonts,
IDictionary<string, string> themeFonts,
ISet<string> usedFonts)
{
foreach (var run in root.Descendants<W.Run>())
{
if (!ContainsVisibleText(run))
{
continue;
}
var effectiveFonts = ResolveEffectiveWordFonts(run, styleIndex, defaultFonts, themeFonts);
foreach (var font in effectiveFonts.GetDefinedFonts())
{
usedFonts.Add(font);
}
}
}
private static void CollectDrawingFonts(OpenXmlElement root, ISet<string> usedFonts)
{
foreach (var element in root.Descendants())
{
if (!(string.Equals(element.LocalName, "latin", StringComparison.OrdinalIgnoreCase)
|| string.Equals(element.LocalName, "ea", StringComparison.OrdinalIgnoreCase)
|| string.Equals(element.LocalName, "cs", StringComparison.OrdinalIgnoreCase)
|| string.Equals(element.LocalName, "font", StringComparison.OrdinalIgnoreCase)))
{
continue;
}
var typeface = NormalizeFontName(GetAttributeValue(element, "typeface"));
if (!string.IsNullOrWhiteSpace(typeface))
{
usedFonts.Add(typeface);
}
}
}
private static bool ContainsVisibleText(W.Run run)
{
if (run == null || string.IsNullOrWhiteSpace(run.InnerText))
{
return false;
}
if (run.Ancestors<W.DeletedRun>().Any())
{
return false;
}
return run.RunProperties == null || run.RunProperties.Vanish == null;
}
private static FontReferenceSet ResolveEffectiveWordFonts(
W.Run run,
IDictionary<string, StyleFontSet> styleIndex,
FontReferenceSet defaultFonts,
IDictionary<string, string> themeFonts)
{
var effectiveFonts = new FontReferenceSet();
effectiveFonts.ApplyOverride(defaultFonts);
var paragraph = run.Ancestors<W.Paragraph>().FirstOrDefault();
if (paragraph != null && paragraph.ParagraphProperties != null)
{
var paragraphStyleId = paragraph.ParagraphProperties.ParagraphStyleId == null
? null
: paragraph.ParagraphProperties.ParagraphStyleId.Val == null
? null
: paragraph.ParagraphProperties.ParagraphStyleId.Val.Value;
effectiveFonts.ApplyOverride(ResolveStyleFonts(paragraphStyleId, styleIndex));
}
var table = run.Ancestors<W.Table>().FirstOrDefault();
if (table != null && table.TableProperties != null && table.TableProperties.TableStyle != null)
{
var tableStyleId = table.TableProperties.TableStyle.Val == null ? null : table.TableProperties.TableStyle.Val.Value;
effectiveFonts.ApplyOverride(ResolveStyleFonts(tableStyleId, styleIndex));
}
if (run.RunProperties != null && run.RunProperties.RunStyle != null)
{
var runStyleId = run.RunProperties.RunStyle.Val == null ? null : run.RunProperties.RunStyle.Val.Value;
effectiveFonts.ApplyOverride(ResolveStyleFonts(runStyleId, styleIndex));
}
effectiveFonts.ApplyOverride(ExtractWordFontSet(run.RunProperties == null ? null : run.RunProperties.RunFonts, themeFonts));
return effectiveFonts;
}
private static FontReferenceSet ResolveStyleFonts(string styleId, IDictionary<string, StyleFontSet> styleIndex)
{
var result = new FontReferenceSet();
if (string.IsNullOrWhiteSpace(styleId) || styleIndex == null || !styleIndex.TryGetValue(styleId, out var style))
{
return result;
}
var resolved = ResolveStyleFonts(styleId, styleIndex, new HashSet<string>(StringComparer.OrdinalIgnoreCase));
result.ApplyOverride(resolved);
return result;
}
private static FontReferenceSet ResolveStyleFonts(
string styleId,
IDictionary<string, StyleFontSet> styleIndex,
ISet<string> visited)
{
var result = new FontReferenceSet();
if (string.IsNullOrWhiteSpace(styleId)
|| styleIndex == null
|| !styleIndex.TryGetValue(styleId, out var style)
|| !visited.Add(styleId))
{
return result;
}
result.ApplyOverride(ResolveStyleFonts(style.BasedOnStyleId, styleIndex, visited));
result.ApplyOverride(style.Fonts);
return result;
}
private static FontReferenceSet ExtractWordFontSet(W.RunFonts runFonts, IDictionary<string, string> themeFonts)
{
var result = new FontReferenceSet();
if (runFonts == null)
{
return result;
}
result.Ascii = ResolveFontName(GetAttributeValue(runFonts, "ascii"), GetAttributeValue(runFonts, "asciiTheme"), themeFonts);
result.HighAnsi = ResolveFontName(GetAttributeValue(runFonts, "hAnsi"), GetAttributeValue(runFonts, "hAnsiTheme"), themeFonts);
result.EastAsia = ResolveFontName(GetAttributeValue(runFonts, "eastAsia"), GetAttributeValue(runFonts, "eastAsiaTheme"), themeFonts);
result.ComplexScript = ResolveFontName(GetAttributeValue(runFonts, "cs"), GetAttributeValue(runFonts, "cstheme"), themeFonts);
if (string.IsNullOrWhiteSpace(result.ComplexScript))
{
result.ComplexScript = ResolveFontName(null, GetAttributeValue(runFonts, "csTheme"), themeFonts);
}
return result;
}
private static string ResolveFontName(string directValue, string themeToken, IDictionary<string, string> themeFonts)
{
var normalizedDirect = NormalizeFontName(directValue);
if (!string.IsNullOrWhiteSpace(normalizedDirect))
{
return normalizedDirect;
}
var normalizedToken = NormalizeFontName(themeToken);
if (!string.IsNullOrWhiteSpace(normalizedToken)
&& themeFonts != null
&& themeFonts.TryGetValue(normalizedToken, out var themeFont))
{
return NormalizeFontName(themeFont);
}
return null;
}
private static string NormalizeFontName(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var normalized = value.Trim().Trim('"', '\'');
if (normalized.StartsWith("@", StringComparison.Ordinal))
{
normalized = normalized.Substring(1);
}
return normalized.Length == 0 ? null : normalized;
}
private static string GetAttributeValue(OpenXmlElement element, string localName)
{
if (element == null || string.IsNullOrWhiteSpace(localName))
{
return null;
}
foreach (var attribute in element.GetAttributes())
{
if (string.Equals(attribute.LocalName, localName, StringComparison.OrdinalIgnoreCase))
{
return attribute.Value;
}
}
return null;
}
private bool IsFontAvailable(string fontName, IDictionary<string, HashSet<string>> documentFontAliases)
{
if (string.IsNullOrWhiteSpace(fontName))
{
return true;
}
var installedFonts = _installedFonts.Value;
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var queue = new Queue<string>();
queue.Enqueue(fontName);
while (queue.Count > 0)
{
var current = NormalizeFontName(queue.Dequeue());
if (string.IsNullOrWhiteSpace(current) || !visited.Add(current))
{
continue;
}
if (installedFonts.Contains(current))
{
return true;
}
EnqueueAliases(current, documentFontAliases, queue);
EnqueueAliases(current, KnownFontAliases.Value, queue);
}
return false;
}
private static Dictionary<string, HashSet<string>> BuildKnownFontAliases()
{
var result = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var group in KnownFontAliasGroups)
{
if (group == null || group.Length < 2)
{
continue;
}
for (var index = 0; index < group.Length; index++)
{
var current = NormalizeFontName(group[index]);
if (string.IsNullOrWhiteSpace(current))
{
continue;
}
for (var inner = 0; inner < group.Length; inner++)
{
if (inner == index)
{
continue;
}
AddAliasPair(result, current, group[inner]);
}
}
}
return result;
}
private static void EnqueueAliases(string fontName, IDictionary<string, HashSet<string>> aliases, Queue<string> queue)
{
if (queue == null || aliases == null || string.IsNullOrWhiteSpace(fontName))
{
return;
}
if (!aliases.TryGetValue(fontName, out var values) || values == null)
{
return;
}
foreach (var value in values)
{
var normalized = NormalizeFontName(value);
if (!string.IsNullOrWhiteSpace(normalized))
{
queue.Enqueue(normalized);
}
}
}
private static void AddAliasPair(IDictionary<string, HashSet<string>> aliases, string left, string right)
{
var normalizedLeft = NormalizeFontName(left);
var normalizedRight = NormalizeFontName(right);
if (string.IsNullOrWhiteSpace(normalizedLeft)
|| string.IsNullOrWhiteSpace(normalizedRight)
|| string.Equals(normalizedLeft, normalizedRight, StringComparison.OrdinalIgnoreCase))
{
return;
}
AddAliasValue(aliases, normalizedLeft, normalizedRight);
AddAliasValue(aliases, normalizedRight, normalizedLeft);
}
private static void AddAliasValue(IDictionary<string, HashSet<string>> aliases, string key, string value)
{
if (!aliases.TryGetValue(key, out var values))
{
values = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
aliases[key] = values;
}
values.Add(value);
}
private sealed class StyleFontSet
{
public string StyleId { get; set; }
public string BasedOnStyleId { get; set; }
public FontReferenceSet Fonts { get; set; }
}
}
public sealed class FontValidationResult
{
public FontValidationResult(IList<string> usedFonts, IList<string> missingFonts)
{
UsedFonts = usedFonts ?? Array.Empty<string>();
MissingFonts = missingFonts ?? Array.Empty<string>();
}
public IList<string> UsedFonts { get; private set; }
public IList<string> MissingFonts { get; private set; }
}
internal sealed class FontReferenceSet
{
public string Ascii { get; set; }
public string HighAnsi { get; set; }
public string EastAsia { get; set; }
public string ComplexScript { get; set; }
public IEnumerable<string> GetDefinedFonts()
{
return new[] { Ascii, HighAnsi, EastAsia, ComplexScript }
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.OrdinalIgnoreCase);
}
public void ApplyOverride(FontReferenceSet other)
{
if (other == null)
{
return;
}
if (!string.IsNullOrWhiteSpace(other.Ascii))
{
Ascii = other.Ascii;
}
if (!string.IsNullOrWhiteSpace(other.HighAnsi))
{
HighAnsi = other.HighAnsi;
}
if (!string.IsNullOrWhiteSpace(other.EastAsia))
{
EastAsia = other.EastAsia;
}
if (!string.IsNullOrWhiteSpace(other.ComplexScript))
{
ComplexScript = other.ComplexScript;
}
}
}
}