first commit
This commit is contained in:
908
DCIT.Core/Services/FontValidationService.cs
Normal file
908
DCIT.Core/Services/FontValidationService.cs
Normal file
@@ -0,0 +1,908 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user