diff --git a/src/EPPlus.DrawingRenderer.Tests/Fonts/WebFontSubstitutionSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Fonts/WebFontSubstitutionSvgTests.cs new file mode 100644 index 000000000..07fc67c59 --- /dev/null +++ b/src/EPPlus.DrawingRenderer.Tests/Fonts/WebFontSubstitutionSvgTests.cs @@ -0,0 +1,250 @@ +using System.Text.RegularExpressions; +using EPPlus.DrawingRenderer.Svg; +using EPPlus.Fonts.OpenType; +using OfficeOpenXml; +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Interfaces.Fonts; + +namespace EPPlus.Export.ImageRenderer.Tests.Fonts +{ + /// + /// Verifies that web font substitution reaches the SVG output. The font engine does not search + /// system directories, so the output is independent of the fonts installed on the machine. + /// + [TestClass] + public class WebFontSubstitutionSvgTests : TestBase + { + private const string SampleText = "This is a line chart with data from the table below. The chart is exported as SVG when exporting to HTML."; + + [TestInitialize] + public void Initialize() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + } + + private static ExcelPackage CreatePackage(Action? configure = null) + { + var p = new ExcelPackage(); + p.Workbook.UseFontEngine(new OpenTypeFontEngine(cfg => + { + cfg.SearchSystemDirectories = false; + if (configure != null) + { + configure(cfg); + } + })); + return p; + } + + private static ExcelShape AddShapeWithText(ExcelPackage p, string fontName) + { + var ws = p.Workbook.Worksheets.Add("Sheet1"); + var shape = ws.Drawings.AddShape("Shape1", eShapeStyle.Rect); + shape.SetSize(300, 200); + var paragraph = shape.TextBody.Paragraphs.Add(SampleText); + paragraph.TextRuns[0].SetFromFont(fontName, 11); + return shape; + } + + /// + /// Returns the first family of the font-family attribute on every text run (tspan) in the svg. + /// The paragraph level (text element) carries the paragraph default font, which runs may override. + /// + private static List GetRunFontFamilies(string svg) + { + var result = new List(); + foreach (Match m in Regex.Matches(svg, "]*?font-family=\"([^\"]*)\"")) + { + var first = m.Groups[1].Value.Split(',')[0].Trim().Trim('\'', '"'); + result.Add(first); + } + return result; + } + + private static void AssertAllFamilies(string svg, string expectedFamily) + { + var matches = Regex.Matches(svg, "]*?font-family=\"([^\"]*)\"[^>]*>([^<]*)"); + Assert.IsTrue(matches.Count > 0, "No text run with a font-family was rendered."); + foreach (Match m in matches) + { + var family = m.Groups[1].Value.Split(',')[0].Trim().Trim('\'', '"'); + Assert.AreEqual(expectedFamily, family, "Text run '" + m.Groups[2].Value + "'"); + } + } + + // ----------------------------------------------------------------------------------------- + // Shapes + // ----------------------------------------------------------------------------------------- + + [TestMethod] + public void Shape_OfficeCloudFont_IsSubstituted() + { + using (var p = CreatePackage()) + { + var svg = AddShapeWithText(p, "Aptos Narrow").ToSvg(); + + AssertAllFamilies(svg, "Calibri"); + Assert.IsFalse(svg.Contains("Aptos Narrow"), "The original font name leaked into the svg."); + } + } + + [TestMethod] + public void Shape_FontNotInTable_IsUnchanged() + { + using (var p = CreatePackage()) + { + var svg = AddShapeWithText(p, "Arial").ToSvg(); + + AssertAllFamilies(svg, "Arial"); + } + } + + [TestMethod] + public void Shape_DocumentTarget_KeepsOriginalFont() + { + using (var p = CreatePackage()) + { + var svg = AddShapeWithText(p, "Aptos Narrow").ToSvg(o => o.FontTarget = FontRenderTarget.Document); + + AssertAllFamilies(svg, "Aptos Narrow"); + } + } + + [TestMethod] + public void Shape_UserSubstitution_IsUsed() + { + using (var p = CreatePackage(cfg => cfg.WebFontSubstitutions["Aptos Narrow"] = "Verdana")) + { + var svg = AddShapeWithText(p, "Aptos Narrow").ToSvg(); + + AssertAllFamilies(svg, "Verdana"); + } + } + + // ----------------------------------------------------------------------------------------- + // Charts + // ----------------------------------------------------------------------------------------- + + [TestMethod] + public void Chart_OfficeCloudFont_IsSubstitutedInAllText() + { + using (var p = CreatePackage()) + { + var ws = p.Workbook.Worksheets.Add("Sheet1"); + LoadItemData(ws); + var chart = ws.Drawings.AddChart("Chart1", eChartType.Line); + chart.Series.Add(ws.Cells["N2:N11"], ws.Cells["K2:K11"]); + chart.SetSize(600, 400); + + chart.Title.Text = "Chart title"; + chart.Title.Font.LatinFont = "Aptos Narrow"; + chart.XAxis.Font.LatinFont = "Aptos Narrow"; + chart.YAxis.Font.LatinFont = "Aptos Narrow"; + chart.Legend.Font.LatinFont = "Aptos Narrow"; + + var svg = chart.ToSvg(); + + //Covers title, axis labels and legend. A failure here points to a text path + //that bypasses the render context's target. + AssertAllFamilies(svg, "Calibri"); + Assert.IsFalse(svg.Contains("Aptos Narrow"), "The original font name leaked into the svg."); + } + } + + /// + /// Measurement is independent of installed fonts: no system fonts and metrics always preferred. + /// + private static void Deterministic(IEpplusFontConfiguration cfg) + { + cfg.MetricsFallback = MetricsFallbackMode.Always; + } + + /// + /// Returns the text of every text run (tspan) in document order. For wrapped text each run is a line. + /// + private static List GetRunTexts(string svg) + { + var result = new List(); + foreach (Match m in Regex.Matches(svg, "]*>([^<]*)")) + { + result.Add(m.Groups[1].Value); + } + return result; + } + + private static ExcelChart AddChartWithFont(ExcelPackage p, string fontName) + { + var ws = p.Workbook.Worksheets.Add("Sheet1"); + LoadItemData(ws); + var chart = ws.Drawings.AddChart("Chart1", eChartType.Line); + chart.Series.Add(ws.Cells["N2:N11"], ws.Cells["K2:K11"]); + chart.SetSize(400, 300); + + chart.Title.Text = "A long chart title that should wrap into more than one line"; + chart.Title.Font.LatinFont = fontName; + chart.XAxis.Font.LatinFont = fontName; + chart.YAxis.Font.LatinFont = fontName; + chart.Legend.Font.LatinFont = fontName; + return chart; + } + + // ----------------------------------------------------------------------------------------- + // Measurement follows the substitution + // ----------------------------------------------------------------------------------------- + + [TestMethod] + public void Shape_WebSubstitution_WrapsAsSubstituteFont() + { + string webSvg, documentSvg; + using (var p = CreatePackage(Deterministic)) + { + webSvg = AddShapeWithText(p, "Aptos Narrow").ToSvg(); + } + using (var p = CreatePackage(Deterministic)) + { + documentSvg = AddShapeWithText(p, "Calibri").ToSvg(o => o.FontTarget = FontRenderTarget.Document); + } + + var webLines = GetRunTexts(webSvg); + Assert.IsTrue(webLines.Count > 1, "The text must wrap for the test to verify line breaking."); + CollectionAssert.AreEqual(GetRunTexts(documentSvg), webLines); + } + + [TestMethod] + public void Shape_WrapComparison_DetectsDifferentFont() + { + //Control for the test above: a clearly wider font must break differently, + //otherwise the comparison would pass regardless of which font is measured. + string webSvg, documentSvg; + using (var p = CreatePackage(Deterministic)) + { + webSvg = AddShapeWithText(p, "Aptos Narrow").ToSvg(); + } + using (var p = CreatePackage(Deterministic)) + { + documentSvg = AddShapeWithText(p, "Verdana").ToSvg(o => o.FontTarget = FontRenderTarget.Document); + } + + CollectionAssert.AreNotEqual(GetRunTexts(documentSvg), GetRunTexts(webSvg)); + } + + [TestMethod] + public void Chart_WebSubstitution_LaysOutAsSubstituteFont() + { + string webSvg, documentSvg; + using (var p = CreatePackage(Deterministic)) + { + webSvg = AddChartWithFont(p, "Aptos Narrow").ToSvg(); + } + using (var p = CreatePackage(Deterministic)) + { + documentSvg = AddChartWithFont(p, "Calibri").ToSvg(o => o.FontTarget = FontRenderTarget.Document); + } + + //Covers title wrapping, axis label selection and legend. Any text path that measures + //with the original font instead of the substitute makes the sequences differ. + CollectionAssert.AreEqual(GetRunTexts(documentSvg), GetRunTexts(webSvg)); + } + } +} \ No newline at end of file diff --git a/src/EPPlus.DrawingRenderer/RenderContext.cs b/src/EPPlus.DrawingRenderer/RenderContext.cs index fa2ebd031..666ad65b8 100644 --- a/src/EPPlus.DrawingRenderer/RenderContext.cs +++ b/src/EPPlus.DrawingRenderer/RenderContext.cs @@ -1,4 +1,6 @@ using EPPlus.Fonts.OpenType; +using OfficeOpenXml.Interfaces.Fonts; +using OfficeOpenXml.Interfaces.RichText; namespace EPPlus.DrawingRenderer { @@ -11,21 +13,53 @@ namespace EPPlus.DrawingRenderer /// public class RenderContext : IDisposable { - private readonly object _lock = new object(); + private readonly object _lock; private readonly Func _engineFactory; - private OpenTypeFontEngine? _fontEngine; + private readonly RenderContext _root; + private volatile OpenTypeFontEngine _fontEngine; public RenderContext(Func engineFactory) { if (engineFactory == null) throw new ArgumentNullException("engineFactory"); _engineFactory = engineFactory; + _lock = new object(); + Target = FontRenderTarget.Document; + } + + /// + /// Creates a view of the root context for another render target. Shares the root's font engine. + /// + private RenderContext(RenderContext root, FontRenderTarget target) + { + _root = root; + Target = target; + } + + /// + /// The output kind text is laid out for. Decides whether web font substitution applies. + /// + public FontRenderTarget Target { get; private set; } + + /// + /// Returns a context for the given target that shares this context's font engine and cache. + /// + public RenderContext ForTarget(FontRenderTarget target) + { + if (target == Target) + return this; + + var root = _root ?? this; + return target == root.Target ? root : new RenderContext(root, target); } public OpenTypeFontEngine FontEngine { get { + if (_root != null) + return _root.FontEngine; + if (_fontEngine == null) { lock (_lock) @@ -38,12 +72,35 @@ public OpenTypeFontEngine FontEngine } } + /// + /// Returns the font family to lay out and output text with for this context's target. + /// + public string GetFamilyForTarget(string fontName) + { + return FontEngine.GetFamilyForTarget(fontName, Target); + } + + /// + /// Gets a measurement shaper for the font, with the family substituted for this context's target. + /// All text measurement in drawing rendering should go through here. + /// + public ITextShaper GetMeasurementShaper(IFontFormatBase font) + { + if (font == null) + throw new ArgumentNullException("font"); + return FontEngine.GetMeasurementShaper(GetFamilyForTarget(font.Family), font.SubFamily); + } public void Dispose() { - if (_fontEngine != null) + // A view does not own the engine. + if (_root != null) + return; + + var engine = _fontEngine; + if (engine != null) { - try { _fontEngine.Dispose(); } catch { /* best effort */ } + try { engine.Dispose(); } catch { /* best effort */ } _fontEngine = null; } } diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index 3b54e7326..09e18a74f 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -96,11 +96,26 @@ public abstract class ParagraphRenderItem : RenderItem public bool AutoSize = false; - public FontFormatBase DefaultParagraphFont; + private FontFormatBase _defaultParagraphFont; + + /// + /// The default font of the paragraph. The family is substituted for the render target on assignment. + /// + public FontFormatBase DefaultParagraphFont + { + get { return _defaultParagraphFont; } + set + { + _defaultParagraphFont = value; + ApplyFontTarget(value); + } + } protected double ParentMaxWidth; protected double ParentMaxHeight; + + protected RenderTextBody ParentTextBody { get; set; } protected double? _lsMultiplier = null; @@ -226,6 +241,17 @@ TextLineCollection WrapFragmentsToLines(List? fragments = nul return _layoutSystem.Wrap(maxWidthInPoints); } + /// + /// Substitutes the font family for the render target. Every font that reaches measurement must pass + /// through here, since text runs output the same font object they were measured with. + /// + protected void ApplyFontTarget(IFontFormatBase font) + { + if (font == null || RenderContext == null) + return; + font.Family = RenderContext.GetFamilyForTarget(font.Family); + } + private void AddRichTextBase(IRichTextFormatSimple rt) { if (_textFragments == null) @@ -235,10 +261,12 @@ private void AddRichTextBase(IRichTextFormatSimple rt) if (string.IsNullOrEmpty(rt.Text) == false) { + ApplyFontTarget(rt); _textFragments.Add(rt); } } + protected void AddDefaultTextFragment(string text) { var defaults = new RichTextFormatSimple(); diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/TextRunRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/TextRunRenderItem.cs index 0b04d4f66..0421f49cf 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/TextRunRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/TextRunRenderItem.cs @@ -172,7 +172,9 @@ internal protected void InitializeBase(IFontFormatBase font) //Should be ascent-only? Bounds.Height = font.Size; FontSizeInPixels = ((double)font.Size).PointToPixel(true); - _measurementFont = font; + //Copy, as callers pass shared instances (e.g. the paragraph default font) and the + //run adjusts its own size for sub- and superscript. + _measurementFont = new FontFormatBase(font.Family, font.SubFamily, font.Size); } /// diff --git a/src/EPPlus.DrawingRenderer/Svg/Options/SvgRenderOptions.cs b/src/EPPlus.DrawingRenderer/Svg/Options/SvgRenderOptions.cs index 542a83d64..e0dc2457f 100644 --- a/src/EPPlus.DrawingRenderer/Svg/Options/SvgRenderOptions.cs +++ b/src/EPPlus.DrawingRenderer/Svg/Options/SvgRenderOptions.cs @@ -10,6 +10,8 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.Interfaces.Fonts; + namespace EPPlus.DrawingRenderer.Svg { /// @@ -31,6 +33,14 @@ public class SvgRenderOptions /// Sets the width and height of the svg image. If not set, the size will be calculated based on the drawings dimensions. /// public SvgSize SvgSize { get; } = new SvgSize(); + + /// + /// The render target used for text layout. Defaults to , which + /// substitutes fonts unlikely to be available to a browser, both when measuring and in the output. + /// Use when the fonts are made available to the consumer + /// by other means, e.g. embedded. + /// + public FontRenderTarget FontTarget { get; set; } = FontRenderTarget.Web; } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/FontResolver/WebFontSubstitutionTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FontResolver/WebFontSubstitutionTests.cs new file mode 100644 index 000000000..cfdb93af7 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/FontResolver/WebFontSubstitutionTests.cs @@ -0,0 +1,195 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 09/28/2026 EPPlus Software AB Web font substitution tests + *************************************************************************************************/ +using OfficeOpenXml.Interfaces.Fonts; + +namespace EPPlus.Fonts.OpenType.Tests.FontResolver +{ + /// + /// Tests for web font substitution via . + /// None of these tests resolve or load fonts, so they are independent of installed fonts. + /// + [TestClass] + public class WebFontSubstitutionTests : FontTestBase + { + public override TestContext? TestContext { get; set; } + + /// + /// Creates an isolated engine for tests that change the configuration. + /// The shared engines in must not be reconfigured. + /// + private static OpenTypeFontEngine CreateEngine(Action? configure = null) + { + return new OpenTypeFontEngine(cfg => + { + cfg.SearchSystemDirectories = false; + if (configure != null) + { + configure(cfg); + } + }); + } + + // ----------------------------------------------------------------------------------------- + // Render target + // ----------------------------------------------------------------------------------------- + + [TestMethod] + public void DocumentTarget_NeverSubstitutes() + { + var family = TestFolderEngine.GetFamilyForTarget("Aptos Narrow", FontRenderTarget.Document); + + Assert.AreEqual("Aptos Narrow", family); + } + + [TestMethod] + public void WebTarget_UnknownFont_IsUnchanged() + { + var family = TestFolderEngine.GetFamilyForTarget("Arial", FontRenderTarget.Web); + + Assert.AreEqual("Arial", family); + } + + [TestMethod] + public void WebTarget_NullOrEmptyFontName_IsReturnedAsIs() + { + Assert.IsNull(TestFolderEngine.GetFamilyForTarget(null!, FontRenderTarget.Web)); + Assert.AreEqual("", TestFolderEngine.GetFamilyForTarget("", FontRenderTarget.Web)); + } + + // ----------------------------------------------------------------------------------------- + // Default substitution table + // ----------------------------------------------------------------------------------------- + + [DataTestMethod] + [DataRow("Aptos", "Arial")] + [DataRow("Aptos Narrow", "Calibri")] + [DataRow("Aptos Display", "Arial")] + [DataRow("Aptos Serif", "Cambria")] + [DataRow("Aptos Mono", "Consolas")] + [DataRow("Grandview", "Calibri")] + [DataRow("Seaford", "Segoe UI")] + [DataRow("Tenorite", "Segoe UI")] + [DataRow("Bierstadt", "Arial")] + [DataRow("Skeena", "Segoe UI")] + public void WebTarget_OfficeCloudFont_IsSubstitutedByDefault(string requested, string expected) + { + var family = TestFolderEngine.GetFamilyForTarget(requested, FontRenderTarget.Web); + + Assert.AreEqual(expected, family); + } + + [DataTestMethod] + [DataRow("aptos narrow")] + [DataRow("APTOS NARROW")] + [DataRow("Aptos NARROW")] + public void WebTarget_Lookup_IsCaseInsensitive(string requested) + { + var family = TestFolderEngine.GetFamilyForTarget(requested, FontRenderTarget.Web); + + Assert.AreEqual("Calibri", family); + } + + // ----------------------------------------------------------------------------------------- + // User configuration + // ----------------------------------------------------------------------------------------- + + [TestMethod] + public void WebTarget_UserSubstitution_OverridesDefault() + { + using (var engine = CreateEngine(cfg => cfg.WebFontSubstitutions["Aptos Narrow"] = "Arial")) + { + Assert.AreEqual("Arial", engine.GetFamilyForTarget("Aptos Narrow", FontRenderTarget.Web)); + } + } + + [TestMethod] + public void WebTarget_UserSubstitution_ForNonDefaultFont_IsApplied() + { + using (var engine = CreateEngine(cfg => cfg.WebFontSubstitutions["Goudy Stout"] = "Georgia")) + { + Assert.AreEqual("Georgia", engine.GetFamilyForTarget("Goudy Stout", FontRenderTarget.Web)); + } + } + + [TestMethod] + public void WebTarget_UserSubstitution_DoesNotAffectDocumentTarget() + { + using (var engine = CreateEngine(cfg => cfg.WebFontSubstitutions["Goudy Stout"] = "Georgia")) + { + Assert.AreEqual("Goudy Stout", engine.GetFamilyForTarget("Goudy Stout", FontRenderTarget.Document)); + } + } + + [DataTestMethod] + [DataRow("")] + [DataRow(null)] + public void WebTarget_EmptySubstitute_KeepsOriginalFont(string? substitute) + { + using (var engine = CreateEngine(cfg => cfg.WebFontSubstitutions["Aptos Narrow"] = substitute!)) + { + Assert.AreEqual("Aptos Narrow", engine.GetFamilyForTarget("Aptos Narrow", FontRenderTarget.Web)); + } + } + + [TestMethod] + public void WebTarget_RemovedDefault_KeepsOriginalFont() + { + using (var engine = CreateEngine(cfg => cfg.WebFontSubstitutions.Remove("Aptos Narrow"))) + { + Assert.AreEqual("Aptos Narrow", engine.GetFamilyForTarget("Aptos Narrow", FontRenderTarget.Web)); + } + } + + [TestMethod] + public void WebTarget_Substitution_IsNotChained() + { + using (var engine = CreateEngine(cfg => + { + cfg.WebFontSubstitutions["Font A"] = "Font B"; + cfg.WebFontSubstitutions["Font B"] = "Font C"; + })) + { + Assert.AreEqual("Font B", engine.GetFamilyForTarget("Font A", FontRenderTarget.Web)); + } + } + + [TestMethod] + public void Reset_RestoresDefaultSubstitutions() + { + using (var engine = CreateEngine(cfg => + { + cfg.WebFontSubstitutions.Clear(); + cfg.WebFontSubstitutions["Goudy Stout"] = "Georgia"; + cfg.Reset(); + })) + { + Assert.AreEqual("Calibri", engine.GetFamilyForTarget("Aptos Narrow", FontRenderTarget.Web)); + Assert.AreEqual("Goudy Stout", engine.GetFamilyForTarget("Goudy Stout", FontRenderTarget.Web)); + } + } + + // ----------------------------------------------------------------------------------------- + // Lifecycle + // ----------------------------------------------------------------------------------------- + + [TestMethod] + [ExpectedException(typeof(ObjectDisposedException))] + public void GetFamilyForTarget_OnDisposedEngine_Throws() + { + var engine = CreateEngine(); + engine.Dispose(); + + engine.GetFamilyForTarget("Aptos Narrow", FontRenderTarget.Web); + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType.Tests/FontScanning/SystemFontsTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FontScanning/SystemFontsTests.cs new file mode 100644 index 000000000..ed234d3cd --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/FontScanning/SystemFontsTests.cs @@ -0,0 +1,23 @@ +using OfficeOpenXml.Interfaces.Fonts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.Fonts.OpenType.Tests.FontScanning +{ + [TestClass] + public class SystemFontsTests : FontTestBase + { + public override TestContext? TestContext { get; set; } + + [TestMethod] + public void AptosNarrowTest1() + { + RequireFont(SystemFontsEngine, "Aptos Narrow", FontSubFamily.Regular); + var font = SystemFontsEngine.LoadFont("Aptos Narrow", FontSubFamily.Regular); + Assert.AreEqual("Aptos Narrow", font.FullName); + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs b/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs index cb38f651a..7aa1d2789 100644 --- a/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs +++ b/src/EPPlus.Fonts.OpenType/EpplusFontConfiguration.cs @@ -49,6 +49,7 @@ public EpplusFontConfiguration() SearchSystemDirectories = true; MetricsFallback = MetricsFallbackMode.WhenFontMissing; ApplyDefaultScriptFallbacks(); + ApplyDefaultWebFontSubstitutions(); } /// @@ -112,6 +113,8 @@ public void Reset() _scriptFallbacks.Clear(); MetricsFallback = MetricsFallbackMode.WhenFontMissing; ApplyDefaultScriptFallbacks(); + _webFontSubstitutions.Clear(); + ApplyDefaultWebFontSubstitutions(); } // ----------------------------------------------------------------------------------------- @@ -195,5 +198,36 @@ private void ApplyDefaultScriptFallbacks() "Mangal", "Nirmala UI", "Noto Sans Devanagari" }; } + + private readonly Dictionary _webFontSubstitutions = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + + + /// + public IDictionary WebFontSubstitutions + { + get { return _webFontSubstitutions; } + } + + /// + /// Office cloud fonts are delivered through Office's font service and cached per user, never + /// installed system-wide. Browsers cannot see them, even on machines where Office has them. + /// Substitutes are fonts shipped with Windows that have metric-compatible open alternatives + /// (Calibri/Carlito, Cambria/Caladea), so server-side measurement stays reproducible. + /// + private void ApplyDefaultWebFontSubstitutions() + { + _webFontSubstitutions["Aptos"] = "Arial"; + _webFontSubstitutions["Aptos Narrow"] = "Calibri"; + _webFontSubstitutions["Aptos Display"] = "Arial"; + _webFontSubstitutions["Aptos Serif"] = "Cambria"; + _webFontSubstitutions["Aptos Mono"] = "Consolas"; + _webFontSubstitutions["Grandview"] = "Calibri"; + _webFontSubstitutions["Seaford"] = "Segoe UI"; + _webFontSubstitutions["Tenorite"] = "Segoe UI"; + _webFontSubstitutions["Bierstadt"] = "Arial"; + _webFontSubstitutions["Skeena"] = "Segoe UI"; + } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs index 0cf3acc98..7cdbced44 100644 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs +++ b/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs @@ -480,5 +480,25 @@ private static bool IsLastResortFamily(string fontName) { return string.Equals("archivo narrow", fontName, StringComparison.OrdinalIgnoreCase); } + + /// + /// Returns the font family to use for the given render target. For + /// the requested family is always returned unchanged. For a + /// configured substitute is returned when one exists. Substitution is a single step and is not chained. + /// + public string GetFamilyForTarget(string fontName, FontRenderTarget target) + { + ThrowIfDisposed(); + if (target != FontRenderTarget.Web || string.IsNullOrEmpty(fontName)) + return fontName; + + string substitute; + if (_configuration.WebFontSubstitutions.TryGetValue(fontName, out substitute) + && string.IsNullOrEmpty(substitute) == false) + { + return substitute; + } + return fontName; + } } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/SvgRenderOptions.cs b/src/EPPlus.Interfaces/Fonts/FontRenderTarget.cs similarity index 58% rename from src/EPPlus/Drawing/SvgRenderOptions.cs rename to src/EPPlus.Interfaces/Fonts/FontRenderTarget.cs index a984fa65d..10be1519d 100644 --- a/src/EPPlus/Drawing/SvgRenderOptions.cs +++ b/src/EPPlus.Interfaces/Fonts/FontRenderTarget.cs @@ -8,19 +8,23 @@ This software is licensed under PolyForm Noncommercial License 1.0.0 ************************************************************************************************* Date Author Change ************************************************************************************************* - 01/27/2020 EPPlus Software AB Initial release EPPlus 5 + 10/07/2026 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ -namespace OfficeOpenXml.Drawing.Svg +namespace OfficeOpenXml.Interfaces.Fonts { /// - /// Options for rendering drawings to svg. + /// The kind of output text is being laid out for. Decides whether web font substitution applies. /// - public class SvgRenderOptions + public enum FontRenderTarget { /// - /// The size of the svg image, if you want to override the default size. - /// - public SvgSize Size { get; } = new SvgSize(); + /// Output where EPPlus controls the fonts, e.g. PDF with embedded fonts. No substitution. + /// + Document, + /// + /// Output rendered by a browser using its own fonts, e.g. SVG and HTML. + /// Fonts unlikely to be available to a browser are substituted. + /// + Web } - } \ No newline at end of file diff --git a/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs b/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs index f2d3f2b3c..39bfb38b3 100644 --- a/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs +++ b/src/EPPlus.Interfaces/Fonts/IEpplusFontConfiguration.cs @@ -107,6 +107,15 @@ public interface IEpplusFontConfiguration /// owner. Only one callback is active; a later call replaces the earlier one. /// void OnFontEmbedding(Func callback); + + /// + /// Font substitutions applied when laying out text for . + /// The substitute is used both for measurement and in the output, so the browser renders with + /// the same font the text was measured with. Pre-populated with Office cloud fonts, which are + /// never installed system-wide and therefore not visible to browsers. Lookup is case-insensitive. + /// Set a value to null or an empty string to keep the original font. + /// + IDictionary WebFontSubstitutions { get; } } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/ExcelShape.cs b/src/EPPlus/Drawing/ExcelShape.cs index 741cb735c..d4e8eba92 100644 --- a/src/EPPlus/Drawing/ExcelShape.cs +++ b/src/EPPlus/Drawing/ExcelShape.cs @@ -97,7 +97,7 @@ internal override void DeleteMe() } public string ToSvg(SvgRenderOptions options) { - var sr = new ShapeRenderer(this); + var sr = new ShapeRenderer(this, options.FontTarget); var sb = new StringBuilder(); var svg = new SvgShapeRenderer(this.GetBoundingBox(), sb, options); svg.Render(sr.RenderItems); diff --git a/src/EPPlus/Drawing/Renderer/Chart/Axis/CategoryAxisScaleCalculator.cs b/src/EPPlus/Drawing/Renderer/Chart/Axis/CategoryAxisScaleCalculator.cs index 1e4fb9561..507f55c95 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/Axis/CategoryAxisScaleCalculator.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/Axis/CategoryAxisScaleCalculator.cs @@ -6,6 +6,7 @@ using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using OfficeOpenXml.Utils.DateUtils; using System; using System.Collections.Generic; @@ -17,27 +18,24 @@ namespace EPPlus.Export.ImageRenderer.Svg.Chart.Util internal class CategoryAxisScaleCalculator { - internal static AxisScale CalculateHorizontalAxisByWidth(ref List values, ITextMeasurer tm, AxisOptions options) + internal static AxisScale CalculateHorizontalAxisByWidth(ref List values, ITextShaper shaper, float fontSize, AxisOptions options) { - var ax = options.Axis; var plotAreaWidth = options.ChartSize.Bounds.Width; - var mf = ax.Font.GetMeasureFont(); - List displayValues = GetUniqueValues(values).Select(x=>(object)x.ToString()).ToList(); + List displayValues = GetUniqueValues(values).Select(x => (object)x.ToString()).ToList(); var uniqeItems = displayValues.Count; - var res = tm.MeasureText(displayValues[0].ToString(), mf); + var textHeight = GetTextHeight(shaper, displayValues[0].ToString(), fontSize); //Get interval for maximum width with vertical text. - var interval = GetMinUnitVerticalText(displayValues.Count, res.Height, plotAreaWidth); - + var interval = GetMinUnitVerticalText(displayValues.Count, textHeight, plotAreaWidth); //Get max text width when using diagonal text - var width = mf.Size * Math.Sqrt(2); - var margin = mf.Size * 0.5; + var width = fontSize * Math.Sqrt(2); + var margin = fontSize * 0.5; if (FitAsVerticalDiagonalText(displayValues.Count, interval, width, margin, plotAreaWidth)) //Check diagonal { - if(FitAsHorizontalText(displayValues, interval, mf, tm, plotAreaWidth)) //Check horizontal + if (FitAsHorizontalText(displayValues, interval, shaper, fontSize, plotAreaWidth)) //Check horizontal { return new AxisScale() { @@ -62,13 +60,13 @@ internal static AxisScale CalculateHorizontalAxisByWidth(ref List values }; } } - if(interval != 1) + if (interval != 1) { var removeCount = interval - 1; var c = (int)Math.Truncate(values.Count / (double)interval); - for(int i=0;i<=c;i++) + for (int i = 0; i <= c; i++) { - for(int j=0;j values TextOrientation = eTextOrientation.Vertical, DisplayValues = displayValues }; - } - internal static AxisScale CalculateVerticalAxisByHeight(ref List values, ITextMeasurer tm, AxisOptions options) + internal static AxisScale CalculateVerticalAxisByHeight(ref List values, ITextShaper shaper, float fontSize, AxisOptions options) { - var ax = options.Axis; var plotAreaHeight = options.ChartSize.Bounds.Height; - var mf = ax.Font.GetMeasureFont(); List displayValues = GetUniqueValues(values).Select(x => (object)x.ToString()).ToList(); var uniqeItems = displayValues.Count; - var res = tm.MeasureText(displayValues[0].ToString(), mf); + var textHeight = GetTextHeight(shaper, displayValues[0].ToString(), fontSize); int interval = 1; - while (FitAsVerticalDiagonalText(displayValues.Count, interval, res.Height, 1.2D, plotAreaHeight)==false) //Check horizontal + while (FitAsVerticalDiagonalText(displayValues.Count, interval, textHeight, 1.2D, plotAreaHeight) == false) //Check horizontal { interval++; } if (interval != 1) { - var removeCount = interval-1; + var removeCount = interval - 1; var c = (int)Math.Truncate(displayValues.Count / (double)interval); for (int i = 0; i <= c; i++) { @@ -128,38 +123,17 @@ internal static AxisScale CalculateVerticalAxisByHeight(ref List values, DisplayValues = displayValues }; } - internal static List GetUniqueValues(List values) - { - var ret = new List(); - var hs = new HashSet(); - foreach(var v in values) - { - if(v is object[]) - { - var s = (object[])v; - var key = s[0].ToString() + s[1].ToString(); - if(hs.Add(key)) - { - ret.Add(s[0]); - } - } - else - { - ret.Add(v); - } - } - return ret; - } - private static bool FitAsHorizontalText(List displayValues, int interval, MeasurementFont mf, ITextMeasurer tm, double plotAreaWidth) + + private static bool FitAsHorizontalText(List displayValues, int interval, ITextShaper shaper, float fontSize, double plotAreaWidth) { - var margin = mf.Size * 0.3; - var width = tm.MeasureText(displayValues[0].ToString(), mf).Width + margin; + var margin = fontSize * 0.3; + var width = GetTextWidth(shaper, displayValues[0].ToString(), fontSize) + margin; var pos = interval; while (pos < displayValues.Count && width < plotAreaWidth) { - width = tm.MeasureText(displayValues[pos].ToString(), mf).Width + margin; - if(width > plotAreaWidth) return false; + width = GetTextWidth(shaper, displayValues[pos].ToString(), fontSize) + margin; + if (width > plotAreaWidth) return false; pos += interval; } return width <= plotAreaWidth; @@ -184,5 +158,49 @@ private static int GetMinUnitVerticalText(int itemCount, double textHeight, doub return interval; } + + internal static List GetUniqueValues(List values) + { + var ret = new List(); + var hs = new HashSet(); + foreach (var v in values) + { + if (v is object[]) + { + var s = (object[])v; + var key = s[0].ToString() + s[1].ToString(); + if (hs.Add(key)) + { + ret.Add(s[0]); + } + } + else + { + ret.Add(v); + } + } + return ret; + } + + /// + /// Width of a single line of text in points. An empty text has no width. + /// + private static double GetTextWidth(ITextShaper shaper, string text, float fontSize) + { + if (string.IsNullOrEmpty(text)) + return 0D; + return shaper.Shape(text).GetWidthInPoints(fontSize); + } + + /// + /// Line height in points, matching the height previously returned by ITextMeasurer. + /// An empty text has no height. + /// + private static double GetTextHeight(ITextShaper shaper, string text, float fontSize) + { + if (string.IsNullOrEmpty(text)) + return 0D; + return shaper.GetLineHeightInPoints(fontSize); + } } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/Renderer/Chart/Axis/DateAxisScaleCalculator.cs b/src/EPPlus/Drawing/Renderer/Chart/Axis/DateAxisScaleCalculator.cs index e0e3c9802..747ad0743 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/Axis/DateAxisScaleCalculator.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/Axis/DateAxisScaleCalculator.cs @@ -4,6 +4,7 @@ using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using OfficeOpenXml.Utils.DateUtils; using System; using System.Collections.Generic; @@ -298,16 +299,37 @@ private static DateTime CeilingToUnit(double value, eTimeUnit unit, int interval }; } - internal static AxisScale CalculateByWidthAllowDiagonal(List values, double min, double max, ITextMeasurer tm, AxisOptions options) + /// + /// Width of a single line of text in points. An empty text has no width. + /// + private static double GetTextWidth(ITextShaper shaper, string text, float fontSize) + { + if (string.IsNullOrEmpty(text)) + return 0D; + return shaper.Shape(text).GetWidthInPoints(fontSize); + } + + /// + /// Line height in points, matching the height previously returned by ITextMeasurer. + /// An empty text has no height. + /// + private static double GetTextHeight(ITextShaper shaper, string text, float fontSize) + { + if (string.IsNullOrEmpty(text)) + return 0D; + return shaper.GetLineHeightInPoints(fontSize); + } + + internal static AxisScale CalculateByWidthAllowDiagonal(List values, double min, double max, ITextShaper shaper, float fontSize, AxisOptions options) { var ax = options.Axis; var plotAreaWidth = options.ChartSize.Bounds.Width; - var mf = ax.Font.GetMeasureFont(); int interval; eTimeUnit unit; string format = GetNumberFormat(options); var minString = DateTime.FromOADate(min).ToString(format); - var res = tm.MeasureText(minString, mf); + var textWidth = GetTextWidth(shaper, minString, fontSize); + var textHeight = GetTextHeight(shaper, minString, fontSize); if (options.LockedInterval.HasValue) { interval = (int)options.LockedInterval.Value; @@ -316,21 +338,21 @@ internal static AxisScale CalculateByWidthAllowDiagonal(List values, dou else { GetStartInterval(values.Count, max - min, out interval, out unit); - if(ax.TextBody.Rotation.HasValue==false || ax.TextBody.Rotation==-1000) + if (ax.TextBody.Rotation.HasValue == false || ax.TextBody.Rotation == -1000) { //Get interval for maximum width with vertical text. - while (FitAsVerticalDiagonalText(min, max, interval, unit, res.Height, res.Height * 0.3, plotAreaWidth) == false) + while (FitAsVerticalDiagonalText(min, max, interval, unit, textHeight, textHeight * 0.3, plotAreaWidth) == false) { AddIntervall(ref interval, ref unit); } //Get max text width when using diagonal text - var width = mf.Size * Math.Sqrt(2); - var margin = mf.Size * 0.5; + var width = fontSize * Math.Sqrt(2); + var margin = fontSize * 0.5; if (FitAsVerticalDiagonalText(min, max, interval, unit, width, margin, plotAreaWidth)) //Check diagonal { - if (FitAsHorizontalText(tm, options, min, max, interval, unit, res.Height, plotAreaWidth)) //Check horizontal + if (FitAsHorizontalText(shaper, fontSize, min, max, interval, unit, plotAreaWidth)) //Check horizontal { return new AxisScale() { @@ -363,14 +385,14 @@ internal static AxisScale CalculateByWidthAllowDiagonal(List values, dou var rot = ax.TextBody.Rotation.Value % 360; var sin = Math.Sin(MathHelper.Radians(rot)); var cos = Math.Cos(MathHelper.Radians(rot)); - var width = res.Width * cos + res.Height * sin; + var width = textWidth * cos + textHeight * sin; //Get interval for maximum width with vertical text. while (FitAsVerticalDiagonalText(min, max, interval, unit, width, width * 0.3, plotAreaWidth) == false) { AddIntervall(ref interval, ref unit); } eTextOrientation orientation; - switch(rot) + switch (rot) { case 45: case 315: @@ -499,20 +521,18 @@ private static void AddIntervall(ref int interval, ref eTimeUnit unit) } } - private static bool FitAsHorizontalText(ITextMeasurer tm, AxisOptions options, double min, double max, int interval, eTimeUnit unit, float height, double width) + private static bool FitAsHorizontalText(ITextShaper shaper, float fontSize, double min, double max, int interval, eTimeUnit unit, double width) { var minMargin = 2; //2 Points var minDate = DateTime.FromOADate(min); var maxDate = DateTime.FromOADate(max); var date = minDate; var horizontalWidth = 0D; - var nf = options.NumberFormat; - var mf = options.Axis.Font.GetMeasureFont(); while (date < maxDate) { - var textWidth = tm.MeasureText(date.ToString(), mf).Width; + var textWidth = GetTextWidth(shaper, date.ToString(), fontSize); horizontalWidth += textWidth + minMargin; - if(horizontalWidth > width) + if (horizontalWidth > width) { return false; } @@ -520,7 +540,7 @@ private static bool FitAsHorizontalText(ITextMeasurer tm, AxisOptions options, d { date = date.AddDays(interval); } - else if(unit == eTimeUnit.Months) + else if (unit == eTimeUnit.Months) { date = minDate.AddDays(1).AddMonths(interval).AddDays(-1); //Extra adds to keep last day of month } @@ -570,14 +590,12 @@ private static bool FitAsVerticalDiagonalText(double min, double max, int interv } } - internal static AxisScale CalculateByWidthHeight(double widthOrHeight, double min, double max, ITextMeasurer tm, AxisOptions options) + internal static AxisScale CalculateByWidthHeight(double widthOrHeight, double min, double max, ITextShaper shaper, float fontSize, AxisOptions options) { - var ax = options.Axis; - var mf = ax.Font.GetMeasureFont(); int interval; eTimeUnit unit; var minString = DateTime.FromOADate(min).ToString(options.NumberFormat); - var res = tm.MeasureText(minString, mf); + var textHeight = GetTextHeight(shaper, minString, fontSize); if (options.LockedInterval.HasValue) { interval = (int)options.LockedInterval.Value; @@ -601,7 +619,7 @@ internal static AxisScale CalculateByWidthHeight(double widthOrHeight, double mi axis_max = max; } //Get interval for maximum width with vertical text. - while (FitAsVerticalDiagonalText(axis_min, axis_max, interval, unit, res.Height, res.Height * 0.3, widthOrHeight) == false) + while (FitAsVerticalDiagonalText(axis_min, axis_max, interval, unit, textHeight, textHeight * 0.3, widthOrHeight) == false) { AddIntervall(ref interval, ref unit); double days; @@ -638,7 +656,6 @@ internal static AxisScale CalculateByWidthHeight(double widthOrHeight, double mi Max = max, TextOrientation = eTextOrientation.Horizontal }; - } } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index e65b58b02..33de52096 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -19,6 +19,7 @@ Date Author Change using EPPlus.Export.Renderer; using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Integration; +using EPPlus.Fonts.OpenType.Integration.DataHolders; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; @@ -175,31 +176,40 @@ private List GetAxisDisplayValues(ExcelChartAxisStandard ax, List highest) + if (lineHeight > highest) { - highest = m.Height; + highest = lineHeight; } break; case eTextOrientation.Diagonal: - var width = (m.Width) * COS45; - if (width > highest) + var diagonalHeight = shaper.Shape(s).GetWidthInPoints(fontSize) * COS45; + if (diagonalHeight > highest) { - highest = width; + highest = diagonalHeight; } break; case eTextOrientation.Vertical: - if (m.Width > highest) + var verticalHeight = shaper.Shape(s).GetWidthInPoints(fontSize); + if (verticalHeight > highest) { - highest = m.Width; + highest = verticalHeight; } break; } @@ -355,14 +365,16 @@ private List GetAxisValueTextBoxes() var ret = new List(); if (Axis.LabelPosition == eTickLabelPosition.None) return ret; + //All labels share the axis font. The shaper is obtained through the render context + //so the family is substituted for the render target, matching the font used in the output. var mf = Axis.Font.GetMeasureFont(); - - var shaper = RenderContext.FontEngine.GetShaperForFont(mf); - var tm = new OpenTypeFontTextMeasurer(shaper); + var shaper = ChartRenderer.RenderContext.GetMeasurementShaper(new FontFormatBase(mf)); + var fontSize = mf.Size; + var lineHeight = shaper.GetLineHeightInPoints(fontSize); var axisStyle = GetAxisStyleEntry(); double maxWidth, maxHeight; - if(Axis.AxisPosition==eAxisPosition.Left || Axis.AxisPosition == eAxisPosition.Right) + if (Axis.AxisPosition == eAxisPosition.Left || Axis.AxisPosition == eAxisPosition.Right) { maxWidth = ChartRenderer.ChartArea.Rectangle.Width / 3; //TODO: Check this value. maxHeight = Rectangle.Height / AxisValues.Count; @@ -385,32 +397,34 @@ private List GetAxisValueTextBoxes() break; default: // custom var radRot = MathHelper.Radians(Axis.TextBody.Rotation.Value); - maxWidth = (Rectangle.Width * Math.Sin(radRot) + Rectangle.Height * Math.Cos(radRot)) ; + maxWidth = (Rectangle.Width * Math.Sin(radRot) + Rectangle.Height * Math.Cos(radRot)); maxHeight = ChartRenderer.ChartArea.Rectangle.Height / 3; //TODO: Check this value. break; } } - double widest=0; + double widest = 0; for (var i = 0; i < AxisValues.Count; i++) { var v = Values[i]; var t = AxisValues[i]; - var m = tm.MeasureText(t, mf); - var ticMarkX = GetAxisItemLeft(i, m); - var ticMarkY = GetAxisItemTop(i, m); - var width = m.Width; - var height = m.Height; + + //An empty label has no size, as with ITextMeasurer. + float width = string.IsNullOrEmpty(t) ? 0f : shaper.Shape(t).GetWidthInPoints(fontSize); + float height = string.IsNullOrEmpty(t) ? 0f : lineHeight; + + var ticMarkX = GetAxisItemLeft(i); + var ticMarkY = GetAxisItemTop(i, width, height); double x, y; - if(LabelOrientation==eTextOrientation.Horizontal) + if (LabelOrientation == eTextOrientation.Horizontal) { - if (Axis.AxisType == eAxisType.Cat || Axis.AxisType==eAxisType.Date) + if (Axis.AxisType == eAxisType.Cat || Axis.AxisType == eAxisType.Date) { x = ticMarkX; y = ticMarkY; } else { - if(Axis.IsVertical) + if (Axis.IsVertical) { x = ticMarkX; if (ChartRenderer.Chart.IsTypeBar()) @@ -424,7 +438,7 @@ private List GetAxisValueTextBoxes() } else { - x = ticMarkX - width / 2; + x = ticMarkX - width / 2; y = ticMarkY; } } @@ -444,7 +458,7 @@ private List GetAxisValueTextBoxes() } else //Top { - y = ticMarkY - 4 - BottomMargin - (height/2 * cos); + y = ticMarkY - 4 - BottomMargin - (height / 2 * cos); } } else @@ -460,17 +474,16 @@ private List GetAxisValueTextBoxes() } } } - - var tb = new DrawingTextBox(Chart, Rectangle.Bounds, x, y, width, height, maxWidth, maxHeight); + + var tb = new DrawingTextBox(ChartRenderer.RenderContext, Chart, Rectangle.Bounds, x, y, width, height, maxWidth, maxHeight); if (LabelOrientation == eTextOrientation.Diagonal) { tb.Rotation = -45; - if(Axis.ActualAxisPosition==eActualAxisPosition.Bottom || Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) + if (Axis.ActualAxisPosition == eActualAxisPosition.Bottom || Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) { tb.TextAnchor = eTextAnchor.End; } } - else if (LabelOrientation == eTextOrientation.Vertical) { tb.Rotation = -90; @@ -482,7 +495,7 @@ private List GetAxisValueTextBoxes() var p = Axis.TextBody.Paragraphs.FirstOrDefault(); - if (p.HorizontalAlignment != eTextAlignment.Center && Axis.AxisType!=eAxisType.Val && (Axis.AxisPosition == eAxisPosition.Bottom || Axis.AxisPosition == eAxisPosition.Top)) + if (p.HorizontalAlignment != eTextAlignment.Center && Axis.AxisType != eAxisType.Val && (Axis.AxisPosition == eAxisPosition.Bottom || Axis.AxisPosition == eAxisPosition.Top)) { //Horizontal axises are always center aligned visually //Should be broken out as input to ImportParagraph instead of changing the base item @@ -491,17 +504,16 @@ private List GetAxisValueTextBoxes() tb.ImportParagraph(p, 0, t); - //tb.TextBody.Paragraphs[0].AddText(v, Axis.Font); tb.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, Axis.Fill, axisStyle?.FillReference.Color, UserSpaceSettings.UserSpaceOnUse_Global, DefaultFillColor); - if(widest < tb.Width) + if (widest < tb.Width) { widest = tb.Width; } ret.Add(tb); } - if(Axis.IsVertical) + if (Axis.IsVertical) { //If the axis is vertical, we need to adjust the left position of the textboxes to align them to the right and not have them overlap with the axis line. if (Axis.AxisPosition == eAxisPosition.Left) @@ -519,7 +531,7 @@ private List GetAxisValueTextBoxes() } } } - else if(LabelOrientation==eTextOrientation.Horizontal && IsCatAx()) //Only apples when labels are horizontally aligned + else if (LabelOrientation == eTextOrientation.Horizontal && IsCatAx()) //Only apples when labels are horizontally aligned { //Align the axis labels according to the label alignment setting. This is only relevant for horizontal axis, vertical axis are always right aligned. var lblAlignment = (Axis as ExcelChartAxisStandard)?.LabelAlignment ?? OfficeOpenXml.eAxisLabelAlignment.Center; @@ -571,7 +583,7 @@ private List GetAxisValueTextBoxes() } } } - else if(LabelOrientation == eTextOrientation.Diagonal) + else if (LabelOrientation == eTextOrientation.Diagonal) { if (!(Axis.CrossingAxis == null || Axis.CrossingAxis.CrossBetween == eCrossBetween.MidCat)) { @@ -602,7 +614,7 @@ private bool IsCatAx() return Axis.AxisType == eAxisType.Cat || (Axis.AxisType == eAxisType.Date && IsDateScale==false); } - private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextMeasurement m) + private double GetAxisItemLeft(int i) { if (Axis.IsVertical) { @@ -645,7 +657,7 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text } } - private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextMeasurement m) + private double GetAxisItemTop(int i, double width, double height) { if (Axis.ActualAxisPosition == eActualAxisPosition.Top || Axis.ActualAxisPosition == eActualAxisPosition.TopSecond) { @@ -655,7 +667,7 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM case eTextOrientation.Diagonal: return Rectangle.Bottom; default: - return Rectangle.Bottom - m.Height - TopMargin; + return Rectangle.Bottom - height - TopMargin; } } else if (Axis.ActualAxisPosition == eActualAxisPosition.Bottom || Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) @@ -665,7 +677,7 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM case eTextOrientation.Vertical: if (Axis.LabelPosition == eTickLabelPosition.Low) { - return Rectangle.Bottom - m.Width; + return Rectangle.Bottom - width; } else { @@ -674,7 +686,7 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM case eTextOrientation.Diagonal: if (Axis.LabelPosition == eTickLabelPosition.Low) { - return Rectangle.Bottom - (m.Width+m.Height)* COS45; + return Rectangle.Bottom - (width + height)* COS45; } else { @@ -683,7 +695,7 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM default: if (Axis.LabelPosition == eTickLabelPosition.Low) { - return Rectangle.Bottom - m.Height; + return Rectangle.Bottom - height; } else if (Axis.LabelPosition == eTickLabelPosition.NextTo) { @@ -700,7 +712,7 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM if (Axis.AxisType == eAxisType.Cat || Axis.AxisType == eAxisType.Date) { var majorHeight = Rectangle.Height / (AxisValues.Count); - return Rectangle.Top + majorHeight * (AxisValues.Count - i) - ((majorHeight / 2) + m.Height / 2); + return Rectangle.Top + majorHeight * (AxisValues.Count - i) - ((majorHeight / 2) + height / 2); } else { @@ -1058,10 +1070,6 @@ internal double GetPositionInPlotarea(double val, bool startValue=false) protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, out double? min, out double? max, out double? majorUnit, out eTimeUnit? dateUnit, out eTextOrientation orientation) { var values = ax.GetAxisValues(out bool isCount, out bool isNumeric, out bool isDate); - //if(isCount == false && isNumeric && ax.AxisType == eAxisType.Cat) - //{ - // IsDateAutoAxis = true; - //} var options = new AxisOptions { LockedMin = ax.MinValue, @@ -1074,31 +1082,27 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, ChartSize = rect }; + //All axis labels share the axis font. The shaper is obtained through the render context + //so the family is substituted for the render target, matching the font used in the output. + var mf = ax.Font.GetMeasureFont(); + var shaper = ChartRenderer.RenderContext.GetMeasurementShaper(new FontFormatBase(mf)); + var fontSize = mf.Size; + if (AutoAxisType == eAxisType.Cat && isCount == false && isDate == false) { AxisScale res; if (ax.IsVertical) { - res = CategoryAxisScaleCalculator.CalculateVerticalAxisByHeight(ref values, ChartRenderer.TextMeasurer, options); - } - else - { - res = CategoryAxisScaleCalculator.CalculateHorizontalAxisByWidth(ref values, ChartRenderer.TextMeasurer, options); - } - if (isNumeric) - { - min = res.Min; - max = res.Max; - majorUnit = res.MajorInterval; - dateUnit = null; + res = CategoryAxisScaleCalculator.CalculateVerticalAxisByHeight(ref values, shaper, fontSize, options); } else { - min = res.Min; - max = res.Max; - majorUnit = res.MajorInterval; - dateUnit = null; + res = CategoryAxisScaleCalculator.CalculateHorizontalAxisByWidth(ref values, shaper, fontSize, options); } + min = res.Min; + max = res.Max; + majorUnit = res.MajorInterval; + dateUnit = null; orientation = res.TextOrientation; return res.DisplayValues; @@ -1111,7 +1115,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, { double d; object ov; - if(v is object[] o) + if (v is object[] o) { ov = o[3]; } @@ -1135,15 +1139,13 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, } var length = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right ? ChartRenderer.Bounds.Height : ChartRenderer.Bounds.Width; //Fix and use plotarea width/height. - if(isCount) + if (isCount) { - majorUnit = 1; - dateUnit = null; - for (int i=1;i <= max;i++) + for (int i = 1; i <= max; i++) { l.Add(i); } - var res = CategoryAxisScaleCalculator.CalculateHorizontalAxisByWidth(ref l, ChartRenderer.TextMeasurer, options); + var res = CategoryAxisScaleCalculator.CalculateHorizontalAxisByWidth(ref l, shaper, fontSize, options); min = res.Min; max = res.Max; @@ -1153,7 +1155,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, return l.ToList(); } - if(ax.AxisType==eAxisType.Val) + if (ax.AxisType == eAxisType.Val) { AdjustminMaxFromChartObjects(ax, ref min, ref max); } @@ -1162,17 +1164,17 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, AxisScale res; if (ax.IsVertical) { - res = DateAxisScaleCalculator.CalculateByWidthHeight(options.ChartSize.Bounds.Height, min ?? 0D, max ?? 0D, ChartRenderer.TextMeasurer, options); + res = DateAxisScaleCalculator.CalculateByWidthHeight(options.ChartSize.Bounds.Height, min ?? 0D, max ?? 0D, shaper, fontSize, options); } else { - if (ax.AxisType==eAxisType.Val) + if (ax.AxisType == eAxisType.Val) { res = DateAxisScaleCalculator.Calculate(min ?? 0D, max ?? 0D, options); } else { - res = DateAxisScaleCalculator.CalculateByWidthAllowDiagonal(values, min ?? 0D, max ?? 0D, ChartRenderer.TextMeasurer, options); + res = DateAxisScaleCalculator.CalculateByWidthAllowDiagonal(values, min ?? 0D, max ?? 0D, shaper, fontSize, options); } } @@ -1187,7 +1189,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, while (dt <= maxDt) { l.Add(dt); - switch(res.MajorDateUnit ?? eTimeUnit.Days) + switch (res.MajorDateUnit ?? eTimeUnit.Days) { case eTimeUnit.Years: dt = dt.AddYears((int)res.MajorInterval); @@ -1205,7 +1207,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, max = res.Max; } else - { + { var res = ValueAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options); for (var v = res.Min; v <= res.Max; v += res.MajorInterval) { @@ -1216,7 +1218,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, min = res.Min; max = res.Max; majorUnit = res.MajorInterval; - dateUnit= null; + dateUnit = null; orientation = eTextOrientation.Horizontal; IsNumericAutoAxis = false; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs index 5dbb781f7..1907ab5c3 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs @@ -193,7 +193,7 @@ private static string GetDefaultChartTitleText(ChartRenderer sc, ExcelChartTitle internal void InitTextBox(double maxWidth, double maxHeight) { - TextBox = new DrawingTextBox(_svgChart.Drawing, _svgChart.ChartArea.Rectangle.Bounds, maxWidth, maxHeight); + TextBox = new DrawingTextBox(ChartRenderer.RenderContext, _svgChart.Drawing, _svgChart.ChartArea.Rectangle.Bounds, maxWidth, maxHeight); if (_title.Rotation != 0) { TextBox.Rotation = _title.Rotation; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index 077bbe8c2..17812b6a1 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -124,12 +124,12 @@ private void CreateDatalabel() if (width > 0 && height > 0) { - DataLabel = new DrawingTextBox(Chart, ChartRenderer.ChartArea.Rectangle.Bounds, x, y, width, height); + DataLabel = new DrawingTextBox(ChartRenderer.RenderContext, Chart, ChartRenderer.ChartArea.Rectangle.Bounds, x, y, width, height); DataLabel.TextBody.AutoSize = false; } else { - DataLabel = new DrawingTextBox(Chart, ChartRenderer.Bounds, ChartRenderer.Bounds.Width, ChartRenderer.Bounds.Height); + DataLabel = new DrawingTextBox(ChartRenderer.RenderContext, Chart, ChartRenderer.Bounds, ChartRenderer.Bounds.Width, ChartRenderer.Bounds.Height); if (x > 0) { DataLabel.Left = x; diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 12f91c352..a20d5d34b 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -181,7 +181,7 @@ internal void ImportDataLabel(ExcelChartStandardSerie serie, ExcelChartDataLabel } } - var txtBox = new DrawingTextBox(Chart, Rectangle.Bounds, maxBounds.Width, maxBounds.Height); + var txtBox = new DrawingTextBox(ChartRenderer.RenderContext, Chart, Rectangle.Bounds, maxBounds.Width, maxBounds.Height); txtBox.ImportTextBodyAndParagraphs(dataLabel.TextBody, false); diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index fa2b90d5e..6203cded0 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -39,7 +39,7 @@ namespace EPPlusImageRenderer { internal class ChartRenderer : d.DrawingRenderer { - public ChartRenderer(ExcelChart chart, SvgRenderOptions options) : base(chart) + public ChartRenderer(ExcelChart chart, SvgRenderOptions options) : base(chart, options.FontTarget) { SetChartArea(options); diff --git a/src/EPPlus/Drawing/Renderer/DrawingRenderer.cs b/src/EPPlus/Drawing/Renderer/DrawingRenderer.cs index 9f1f5d2a3..1e77cb12e 100644 --- a/src/EPPlus/Drawing/Renderer/DrawingRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/DrawingRenderer.cs @@ -21,23 +21,21 @@ Date Author Change using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Export.HtmlExport; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Interfaces.Fonts; using System.Collections.Generic; namespace OfficeOpenXml.Drawing.Renderer { internal abstract class DrawingRenderer { - internal DrawingRenderer(ExcelDrawing drawing) + internal DrawingRenderer(ExcelDrawing drawing, FontRenderTarget target = FontRenderTarget.Document) { Drawing = drawing; Bounds = drawing.GetBoundingBox(); var wb = drawing._drawings.Worksheet.Workbook; Theme = wb.ThemeManager.GetOrCreateTheme(); - RenderContext = wb.RenderContext; - - var shaper = RenderContext.FontEngine.GetTextShaper(Theme.FontScheme.MajorFont[0].Typeface); - TextMeasurer = new OpenTypeFontTextMeasurer(shaper); + RenderContext = wb.RenderContext.ForTarget(target); } @@ -54,7 +52,7 @@ internal DrawingRenderer() public ExcelDrawing Drawing { get; } public ExcelTheme Theme { get;} public ExcelWorkbook Workbook => Drawing._drawings.Worksheet.Workbook; - internal ITextMeasurer TextMeasurer { get; } + //internal ITextMeasurer TextMeasurer { get; } internal RenderContext RenderContext { get; } public List RenderItems { get; } = new List(); internal BoundingBox Bounds = new BoundingBox(); diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs index 74895341e..c955307d2 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs @@ -119,12 +119,14 @@ internal void ImportLinesAndTextRunsDefault(string textIfEmpty, ExcelTextFont fo /// So that we can easily know what textfragment is on what line and what size it has later /// /// - void GenerateRichText(ExcelDrawingTextRunCollection runs/*, List? optionLst = null*/) + void GenerateRichText(ExcelDrawingTextRunCollection runs) { - //var lstOfRichText = runs.ExportToOpenTypeFormat(); var lstOfRichText = runs.ExportToImageRendererFormat(); foreach (var rt in lstOfRichText) { + //Added directly, not via AddRichTextBase, as ImportStyles relies on a 1:1 index + //between the paragraph's text runs and the fragments, including empty runs. + ApplyFontTarget(rt); _textFragments.Add(rt); } } @@ -242,7 +244,7 @@ private void ImportAlignment(bool isAutoSize, double maxWidth, double parentWidt private void ImportLineSpacing(eDrawingTextLineSpacing lsType, double lineSpacingValue) { _lsType = (TextLineSpacing)lsType; - var shaper = (ITextShaper)RenderContext.FontEngine.GetShaperForFont(DefaultParagraphFont); + var shaper = RenderContext.GetMeasurementShaper(DefaultParagraphFont); ParagraphLineSpacing = GetParagraphLineSpacingInPoints( lineSpacingValue, diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs index 13a6eac20..c0aa7737a 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -1,39 +1,51 @@ -using EPPlus.Graphics; -using OfficeOpenXml.Drawing; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; -using OfficeOpenXml.Style; -using OfficeOpenXml.Utils.EnumUtils; -using System; -using System.Collections.Generic; +using EPPlus.DrawingRenderer; using EPPlus.DrawingRenderer.RenderItems; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Graphics; +using OfficeOpenXml.Style; +using System; + namespace OfficeOpenXml.Drawing.Renderer.TextBox { public class DrawingTextBox : RenderTextbox { ExcelDrawing _drawing; - internal DrawingTextBox(ExcelDrawing drawing, BoundingBox parent, double left, double top, double width, double height, double maxWidth = double.NaN, double maxHeight = double.NaN) : base(parent, left, top, width, height, maxWidth, maxHeight) + + /// + /// Creates a text box at a given position. + /// + /// The render context of the renderer creating the text box. Decides the font render target. + internal DrawingTextBox(RenderContext renderContext, ExcelDrawing drawing, BoundingBox parent, double left, double top, double width, double height, double maxWidth = double.NaN, double maxHeight = double.NaN) + : base(parent, left, top, width, height, maxWidth, maxHeight) { - Init(drawing, parent, maxWidth, maxHeight); + Init(renderContext, drawing, parent, maxWidth, maxHeight); Left = left; Top = top; } - private void Init(ExcelDrawing drawing, BoundingBox parent, double maxWidth, double maxHeight) + /// + /// Creates a text box to be positioned later. + /// + /// The render context of the renderer creating the text box. Decides the font render target. + internal DrawingTextBox(RenderContext renderContext, ExcelDrawing drawing, BoundingBox parent, double maxWidth, double maxHeight) + : base(parent, maxWidth, maxHeight) + { + Init(renderContext, drawing, parent, maxWidth, maxHeight); + } + + private void Init(RenderContext renderContext, ExcelDrawing drawing, BoundingBox parent, double maxWidth, double maxHeight) { + if (renderContext == null) + throw new ArgumentNullException("renderContext"); + Parent = parent; - _drawing= drawing; - var renderContext = drawing._drawings.Worksheet.Workbook.RenderContext; + _drawing = drawing; + //The context must come from the renderer, not the workbook, so the render target is preserved. TextBody = new DrawingTextBody(renderContext, drawing, _marginGroup.Bounds, true); TextBody.MaxWidth = maxWidth; TextBody.MaxHeight = maxHeight; } - internal DrawingTextBox(ExcelDrawing drawing, BoundingBox parent, double maxWidth, double maxHeight) : base(parent, maxWidth, maxHeight) - { - Init(drawing, parent, maxWidth, maxHeight); - } - internal void AddText(string text = null) { TextBody.AddParagraph(text); @@ -76,10 +88,5 @@ internal void ImportParagraph(ExcelDrawingParagraph item, double startingY, stri { _textBody.ImportParagraph(item, startingY, text); } - - //internal void AddText(double startingY, string text = null) - //{ - // TextBody.AddParagraph(startingY, text); - //} } -} +} \ No newline at end of file diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextRunItem.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextRunItem.cs index 39310b537..829fae671 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextRunItem.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextRunItem.cs @@ -29,14 +29,12 @@ internal DrawingTextRunRenderItem(BoundingBox parent, string text, int origRtInd } /// - /// TextRunBase holds style info - /// baseFont is most likely a OpenTypeFontInfoBase made out of the font but we don't want to 'new' it every time we import + /// Imports style info from the text run. The font is taken from the fragment the text was + /// measured with, never from the model, so measurement and output cannot diverge. /// - /// - /// - internal void ImportTextRunBase(ExcelParagraphTextRunBase run, IFontFormatBase baseFont) + internal void ImportTextRunBase(ExcelParagraphTextRunBase run, IFontFormatBase measuredFont) { - InitializeBase(new FontFormatBase(run.GetMeasurementFont())); + InitializeBase(measuredFont); _currentText = string.IsNullOrEmpty(_currentText) ? run.Text : _currentText; _isFirstInParagraph = run.IsFirstInParagraph; _baseline = run.Baseline; diff --git a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs index 81f399db8..92078702c 100644 --- a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs @@ -22,8 +22,9 @@ Date Author Change using EPPlusImageRenderer.RenderItems; using OfficeOpenXml; using OfficeOpenXml.Drawing.Renderer.TextBox; -using OfficeOpenXml.Utils.TypeConversion; +using OfficeOpenXml.Interfaces.Fonts; using OfficeOpenXml.Utils.Drawing; +using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; using System.Linq; @@ -52,7 +53,7 @@ internal class ShapeRenderer : DrawingRenderer //} - public ShapeRenderer(ExcelShape shape) : base(shape) + public ShapeRenderer(ExcelShape shape, FontRenderTarget target) : base(shape, target) { var style = shape.Style; diff --git a/src/EPPlusTest/ExcelPackageTests.cs b/src/EPPlusTest/ExcelPackageTests.cs index 25937ef3e..d21483c5f 100644 --- a/src/EPPlusTest/ExcelPackageTests.cs +++ b/src/EPPlusTest/ExcelPackageTests.cs @@ -26,16 +26,12 @@ Date Author Change ******************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *******************************************************************************/ -using EPPlusTest.Drawing.Chart.Styling; -using FakeItEasy.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml; using OfficeOpenXml.Constants; using System; -using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; namespace EPPlusTest