Skip to content

Commit 49fa8f6

Browse files
committed
wip
1 parent 7e8f6eb commit 49fa8f6

8 files changed

Lines changed: 363 additions & 38 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*************************************************************************************************
2+
Required Notice: Copyright (C) EPPlus Software AB.
3+
This software is licensed under PolyForm Noncommercial License 1.0.0
4+
and may only be used for noncommercial purposes
5+
https://polyformproject.org/licenses/noncommercial/1.0.0/
6+
7+
A commercial license to use this software can be purchased at https://epplussoftware.com
8+
*************************************************************************************************
9+
Date Author Change
10+
*************************************************************************************************
11+
10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0
12+
*************************************************************************************************/
13+
using System;
14+
using System.Collections.Generic;
15+
16+
namespace EPPlus.Fonts.OpenType
17+
{
18+
/// <summary>
19+
/// Custom font provider where user defines their own fallback chain.
20+
/// Does NOT use embedded Noto Emoji - user must add fallbacks manually.
21+
/// </summary>
22+
public class CustomFontProvider : IFontProvider
23+
{
24+
private readonly OpenTypeFont _primaryFont;
25+
private readonly List<OpenTypeFont> _fallbackFonts;
26+
27+
public OpenTypeFont PrimaryFont
28+
{
29+
get { return _primaryFont; }
30+
}
31+
32+
/// <summary>
33+
/// Creates a custom font provider without any fallbacks.
34+
/// </summary>
35+
/// <param name="primaryFont">The user's primary font</param>
36+
public CustomFontProvider(OpenTypeFont primaryFont)
37+
{
38+
if (primaryFont == null)
39+
throw new ArgumentNullException("primaryFont");
40+
41+
_primaryFont = primaryFont;
42+
_fallbackFonts = new List<OpenTypeFont>();
43+
}
44+
45+
/// <summary>
46+
/// Adds a fallback font to the chain.
47+
/// Fonts are searched in the order they are added.
48+
/// </summary>
49+
/// <param name="font">Fallback font to add</param>
50+
public void AddFallback(OpenTypeFont font)
51+
{
52+
if (font == null)
53+
throw new ArgumentNullException("font");
54+
55+
_fallbackFonts.Add(font);
56+
}
57+
58+
public bool TryGetGlyphFont(uint codePoint, out OpenTypeFont font, out ushort glyphId)
59+
{
60+
// Try primary font first
61+
if (_primaryFont.CmapTable.TryGetGlyphId(codePoint, out glyphId))
62+
{
63+
font = _primaryFont;
64+
return true;
65+
}
66+
67+
// Try fallback fonts in order
68+
foreach (var fallbackFont in _fallbackFonts)
69+
{
70+
if (fallbackFont.CmapTable.TryGetGlyphId(codePoint, out glyphId))
71+
{
72+
font = fallbackFont;
73+
return true;
74+
}
75+
}
76+
77+
// Not found - return primary with .notdef
78+
font = _primaryFont;
79+
glyphId = 0;
80+
return false;
81+
}
82+
83+
public IEnumerable<OpenTypeFont> GetAllFonts()
84+
{
85+
yield return _primaryFont;
86+
87+
foreach (var fallback in _fallbackFonts)
88+
{
89+
yield return fallback;
90+
}
91+
}
92+
}
93+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*************************************************************************************************
2+
Required Notice: Copyright (C) EPPlus Software AB.
3+
This software is licensed under PolyForm Noncommercial License 1.0.0
4+
and may only be used for noncommercial purposes
5+
https://polyformproject.org/licenses/noncommercial/1.0.0/
6+
7+
A commercial license to use this software can be purchased at https://epplussoftware.com
8+
*************************************************************************************************
9+
Date Author Change
10+
*************************************************************************************************
11+
10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0
12+
*************************************************************************************************/
13+
using System;
14+
using System.Collections.Generic;
15+
16+
namespace EPPlus.Fonts.OpenType
17+
{
18+
/// <summary>
19+
/// Default font provider with automatic Noto Emoji fallback.
20+
/// </summary>
21+
public class DefaultFontProvider : IFontProvider
22+
{
23+
private readonly OpenTypeFont _primaryFont;
24+
private OpenTypeFont _emojiFont;
25+
private readonly object _lock = new object();
26+
27+
public OpenTypeFont PrimaryFont
28+
{
29+
get { return _primaryFont; }
30+
}
31+
32+
/// <summary>
33+
/// Creates a font provider with automatic emoji fallback.
34+
/// </summary>
35+
/// <param name="primaryFont">The user's primary font</param>
36+
public DefaultFontProvider(OpenTypeFont primaryFont)
37+
{
38+
if (primaryFont == null)
39+
throw new ArgumentNullException("primaryFont");
40+
41+
_primaryFont = primaryFont;
42+
}
43+
44+
/// <summary>
45+
/// Gets the emoji font, loading it on first access (lazy loading).
46+
/// Thread-safe for .NET 3.5 compatibility.
47+
/// </summary>
48+
private OpenTypeFont GetEmojiFontLazy()
49+
{
50+
if (_emojiFont == null)
51+
{
52+
lock (_lock)
53+
{
54+
if (_emojiFont == null)
55+
{
56+
_emojiFont = EmbeddedFonts.LoadNotoEmoji();
57+
}
58+
}
59+
}
60+
return _emojiFont;
61+
}
62+
63+
public bool TryGetGlyphFont(uint codePoint, out OpenTypeFont font, out ushort glyphId)
64+
{
65+
// Try primary font first
66+
if (_primaryFont.CmapTable.TryGetGlyphId(codePoint, out glyphId))
67+
{
68+
font = _primaryFont;
69+
return true;
70+
}
71+
72+
// Fallback to embedded Noto Emoji (lazy-loaded)
73+
var emojiFont = GetEmojiFontLazy();
74+
if (emojiFont.CmapTable.TryGetGlyphId(codePoint, out glyphId))
75+
{
76+
font = emojiFont;
77+
return true;
78+
}
79+
80+
// Not found - return primary with .notdef
81+
font = _primaryFont;
82+
glyphId = 0;
83+
return false;
84+
}
85+
86+
public IEnumerable<OpenTypeFont> GetAllFonts()
87+
{
88+
yield return _primaryFont;
89+
90+
// Only include emoji font if it was actually loaded
91+
if (_emojiFont != null)
92+
{
93+
yield return _emojiFont;
94+
}
95+
}
96+
}
97+
}

src/EPPlus.Fonts.OpenType/EPPlus.Fonts.OpenType.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@
6060
<ItemGroup>
6161
<Compile Remove="TtfFont.cs" />
6262
</ItemGroup>
63+
<ItemGroup>
64+
<None Remove="Resources\NotoEmoji-Regular.ttf" />
65+
</ItemGroup>
66+
<ItemGroup>
67+
<EmbeddedResource Include="Resources\NotoEmoji-Regular.ttf">
68+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
69+
</EmbeddedResource>
70+
</ItemGroup>
6371
<ItemGroup>
6472
<None Include="EPPlusLogo.png">
6573
<Pack>True</Pack>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using EPPlus.Fonts.OpenType.Scanner;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Reflection;
6+
7+
namespace EPPlus.Fonts.OpenType
8+
{
9+
/// <summary>
10+
/// Loads embedded fallback fonts.
11+
/// Internal - not exposed to users.
12+
/// </summary>
13+
internal static class EmbeddedFonts
14+
{
15+
private static readonly Dictionary<string, OpenTypeFont> _cache =
16+
new Dictionary<string, OpenTypeFont>();
17+
18+
private static readonly object _lock = new object();
19+
20+
/// <summary>
21+
/// Loads Noto Emoji Regular (embedded resource).
22+
/// Cached after first load.
23+
/// </summary>
24+
internal static OpenTypeFont LoadNotoEmoji()
25+
{
26+
return LoadCached("NotoEmoji-Regular.ttf");
27+
}
28+
29+
private static OpenTypeFont LoadCached(string resourceName)
30+
{
31+
lock (_lock)
32+
{
33+
if (_cache.TryGetValue(resourceName, out var font))
34+
return font;
35+
36+
var assembly = Assembly.GetExecutingAssembly();
37+
var fullResourceName = $"EPPlus.Fonts.OpenType.Resources.{resourceName}";
38+
39+
using (var stream = assembly.GetManifestResourceStream(fullResourceName))
40+
{
41+
if (stream == null)
42+
{
43+
throw new InvalidOperationException(
44+
$"Embedded font resource not found: {resourceName}. " +
45+
"This is a bug in EPPlus.Fonts.OpenType - please report it.");
46+
}
47+
48+
font = OpenTypeFonts.GetFromBytes(bytes: ReadStreamFully(stream), FontFormat.Ttf);
49+
_cache[resourceName] = font;
50+
return font;
51+
}
52+
}
53+
}
54+
55+
private static byte[] ReadStreamFully(Stream stream)
56+
{
57+
throw new NotImplementedException();
58+
}
59+
}
60+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*************************************************************************************************
2+
Required Notice: Copyright (C) EPPlus Software AB.
3+
This software is licensed under PolyForm Noncommercial License 1.0.0
4+
and may only be used for noncommercial purposes
5+
https://polyformproject.org/licenses/noncommercial/1.0.0/
6+
7+
A commercial license to use this software can be purchased at https://epplussoftware.com
8+
*************************************************************************************************
9+
Date Author Change
10+
*************************************************************************************************
11+
10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0
12+
*************************************************************************************************/
13+
using System.Collections.Generic;
14+
15+
namespace EPPlus.Fonts.OpenType
16+
{
17+
/// <summary>
18+
/// Provides fonts for text shaping with fallback support.
19+
/// </summary>
20+
public interface IFontProvider
21+
{
22+
/// <summary>
23+
/// Gets the primary font (user's chosen font).
24+
/// </summary>
25+
OpenTypeFont PrimaryFont { get; }
26+
27+
/// <summary>
28+
/// Tries to find a font that contains the specified code point.
29+
/// Searches primary font first, then fallbacks.
30+
/// </summary>
31+
/// <param name="codePoint">Unicode code point</param>
32+
/// <param name="font">The font containing the glyph</param>
33+
/// <param name="glyphId">The glyph ID in that font</param>
34+
/// <returns>True if a font was found that contains the glyph</returns>
35+
bool TryGetGlyphFont(uint codePoint, out OpenTypeFont font, out ushort glyphId);
36+
37+
/// <summary>
38+
/// Gets all fonts in the provider (primary + fallbacks).
39+
/// Used for subsetting and PDF embedding.
40+
/// </summary>
41+
IEnumerable<OpenTypeFont> GetAllFonts();
42+
}
43+
}
860 KB
Binary file not shown.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Noto Emoji Font
2+
Copyright � 2013-2024 Google Inc.
3+
Licensed under the SIL Open Font License, Version 1.1
4+
5+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
6+
This license is available with a FAQ at: https://scripts.sil.org/OFL
7+
8+
The font and its source code are available at:
9+
https://github.com/googlefonts/noto-emoji

0 commit comments

Comments
 (0)