[RT]
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
namespace QWERTYkez.WordProcessor;
|
||||
|
||||
internal static class FontPropsExtractor
|
||||
{
|
||||
// ============================================================
|
||||
// Создаёт FontProps на основе Run + разрешает унаследованный шрифт.
|
||||
// ============================================================
|
||||
internal static FontProps CreateFontPropsFromRun(Run run, Paragraph paragraph)
|
||||
{
|
||||
var runProperties = run?.RunProperties;
|
||||
var fontProps = new FontProps();
|
||||
|
||||
if (runProperties is not null)
|
||||
{
|
||||
var runFonts = runProperties.GetFirstChild<RunFonts>();
|
||||
var ff = ExtractFontFamily(runFonts, paragraph);
|
||||
if (!string.IsNullOrEmpty(ff))
|
||||
fontProps = fontProps with { FontFamily = ff };
|
||||
|
||||
var fontSize = runProperties.GetFirstChild<FontSize>();
|
||||
if (fontSize is not null && !string.IsNullOrEmpty(fontSize.Val) &&
|
||||
uint.TryParse(fontSize.Val, out uint halfPoints))
|
||||
{
|
||||
fontProps = fontProps with { Size = halfPoints / 2.0 };
|
||||
}
|
||||
|
||||
var bold = runProperties.GetFirstChild<Bold>();
|
||||
if (bold is not null)
|
||||
{
|
||||
bool isBold = bold.Val?.Value ?? true;
|
||||
fontProps = fontProps with { IsBold = isBold };
|
||||
}
|
||||
|
||||
var italic = runProperties.GetFirstChild<Italic>();
|
||||
if (italic is not null)
|
||||
{
|
||||
bool isItalic = italic.Val?.Value ?? true;
|
||||
fontProps = fontProps with { IsItalic = isItalic };
|
||||
}
|
||||
|
||||
var underline = runProperties.GetFirstChild<Underline>();
|
||||
if (underline?.Val is not null)
|
||||
fontProps = fontProps with { Underline = underline.Val.Value };
|
||||
|
||||
var color = runProperties.GetFirstChild<Color>();
|
||||
if (color is not null && !string.IsNullOrEmpty(color.Val))
|
||||
fontProps = fontProps with { Color = color.Val };
|
||||
|
||||
var verticalAlignment = runProperties.GetFirstChild<VerticalTextAlignment>();
|
||||
if (verticalAlignment?.Val is not null)
|
||||
fontProps = fontProps with { SubSup = verticalAlignment.Val.Value };
|
||||
}
|
||||
|
||||
// Фолбэк: если в run шрифт не задан — идём по цепочке стилей
|
||||
if (string.IsNullOrEmpty(fontProps.FontFamily))
|
||||
{
|
||||
var fallback = ResolveInheritedFontFamily(paragraph);
|
||||
if (!string.IsNullOrEmpty(fallback))
|
||||
fontProps = fontProps with { FontFamily = fallback };
|
||||
}
|
||||
|
||||
return fontProps;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Разрешение шрифта по цепочке:
|
||||
// pStyle → BasedOn → default paragraph style → DocDefaults
|
||||
// ============================================================
|
||||
private static string? ResolveInheritedFontFamily(Paragraph paragraph)
|
||||
{
|
||||
if (paragraph is null) return null;
|
||||
|
||||
var mainPart = paragraph.Ancestors<Document>().FirstOrDefault()?.OpenXmlPart as MainDocumentPart;
|
||||
var styles = mainPart?.StyleDefinitionsPart?.Styles;
|
||||
|
||||
// 1. Стиль абзаца
|
||||
var pStyleId = paragraph.ParagraphProperties?.ParagraphStyleId?.Val?.Value;
|
||||
if (!string.IsNullOrEmpty(pStyleId) && styles is not null)
|
||||
{
|
||||
var ff = ResolveFromStyleChain(styles, pStyleId, mainPart);
|
||||
if (!string.IsNullOrEmpty(ff)) return ff;
|
||||
}
|
||||
|
||||
// 2. Дефолтный стиль абзаца
|
||||
if (styles is not null)
|
||||
{
|
||||
var defaultParaStyle = styles.Elements<Style>()
|
||||
.FirstOrDefault(s => s.Default?.Value == true && s.Type?.Value == StyleValues.Paragraph);
|
||||
var defId = defaultParaStyle?.StyleId?.Value;
|
||||
if (!string.IsNullOrEmpty(defId))
|
||||
{
|
||||
var ff = ResolveFromStyleChain(styles, defId, mainPart);
|
||||
if (!string.IsNullOrEmpty(ff)) return ff;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. DocDefaults
|
||||
var docDefaultsFonts = styles?.DocDefaults?
|
||||
.RunPropertiesDefault?
|
||||
.RunPropertiesBaseStyle?
|
||||
.RunFonts;
|
||||
var docDefault = ExtractFontFamily(docDefaultsFonts, paragraph);
|
||||
if (!string.IsNullOrEmpty(docDefault)) return docDefault;
|
||||
|
||||
// 4. Theme fallback (minor)
|
||||
var themeFont = TryResolveThemeFont(mainPart, isMajor: false);
|
||||
if (!string.IsNullOrEmpty(themeFont)) return themeFont;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ResolveFromStyleChain(Styles styles, string styleId, MainDocumentPart? mainPart)
|
||||
{
|
||||
var visited = new HashSet<string>();
|
||||
var currentId = styleId;
|
||||
|
||||
while (!string.IsNullOrEmpty(currentId) && visited.Add(currentId))
|
||||
{
|
||||
var style = styles.Elements<Style>()
|
||||
.FirstOrDefault(s => s.StyleId?.Value == currentId);
|
||||
if (style is null) break;
|
||||
|
||||
var fonts = style.StyleRunProperties?.RunFonts;
|
||||
var ff = ExtractFontFamily(fonts, mainPart);
|
||||
if (!string.IsNullOrEmpty(ff)) return ff;
|
||||
|
||||
// linked style (w:link) — иногда шрифт лежит в парном character-style
|
||||
var linkedId = style.LinkedStyle?.Val?.Value;
|
||||
if (!string.IsNullOrEmpty(linkedId) && visited.Add(linkedId))
|
||||
{
|
||||
var linked = styles.Elements<Style>()
|
||||
.FirstOrDefault(s => s.StyleId?.Value == linkedId);
|
||||
var linkedFf = ExtractFontFamily(linked?.StyleRunProperties?.RunFonts, mainPart);
|
||||
if (!string.IsNullOrEmpty(linkedFf)) return linkedFf;
|
||||
}
|
||||
|
||||
currentId = style.BasedOn?.Val?.Value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// RunFonts → строка. Разрешает темы.
|
||||
// ============================================================
|
||||
internal static string? ExtractFontFamily(RunFonts? fonts, Paragraph? paragraph)
|
||||
=> ExtractFontFamily(fonts, paragraph?.Ancestors<Document>().FirstOrDefault()?.OpenXmlPart as MainDocumentPart);
|
||||
|
||||
internal static string? ExtractFontFamily(RunFonts? fonts, MainDocumentPart? mainPart)
|
||||
{
|
||||
if (fonts is null) return null;
|
||||
|
||||
if (!string.IsNullOrEmpty(fonts.Ascii)) return fonts.Ascii;
|
||||
if (!string.IsNullOrEmpty(fonts.HighAnsi)) return fonts.HighAnsi;
|
||||
if (!string.IsNullOrEmpty(fonts.ComplexScript)) return fonts.ComplexScript;
|
||||
|
||||
// Атрибуты темы
|
||||
bool isMajor =
|
||||
fonts.AsciiTheme?.Value == ThemeFontValues.MajorAscii ||
|
||||
fonts.AsciiTheme?.Value == ThemeFontValues.MajorHighAnsi ||
|
||||
fonts.HighAnsiTheme?.Value == ThemeFontValues.MajorAscii ||
|
||||
fonts.HighAnsiTheme?.Value == ThemeFontValues.MajorHighAnsi;
|
||||
|
||||
bool hasTheme =
|
||||
fonts.AsciiTheme?.Value is not null ||
|
||||
fonts.HighAnsiTheme?.Value is not null;
|
||||
|
||||
if (hasTheme)
|
||||
{
|
||||
var t = TryResolveThemeFont(mainPart, isMajor);
|
||||
if (!string.IsNullOrEmpty(t)) return t;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? TryResolveThemeFont(MainDocumentPart? mainPart, bool isMajor)
|
||||
{
|
||||
var fontScheme = mainPart?.ThemePart?.Theme?.ThemeElements?.FontScheme;
|
||||
if (fontScheme is null) return null;
|
||||
|
||||
var latin = isMajor
|
||||
? fontScheme.MajorFont?.LatinFont
|
||||
: fontScheme.MinorFont?.LatinFont;
|
||||
|
||||
return latin?.Typeface?.Value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user