Skip to content

Commit 3eb7d9e

Browse files
committed
wip
1 parent 1ed9d88 commit 3eb7d9e

8 files changed

Lines changed: 368 additions & 140 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*************************************************************************************************
2+
Font Provider Unit Tests
3+
Tests for automatic emoji fallback functionality
4+
*************************************************************************************************/
5+
using EPPlus.Fonts.OpenType.TextShaping;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
using System.Linq;
8+
9+
namespace EPPlus.Fonts.OpenType.Tests.FallbackFonts
10+
{
11+
[TestClass]
12+
public class FontProviderTests : FontTestBase
13+
{
14+
public override TestContext? TestContext { get; set; }
15+
16+
private OpenTypeFont _robotoFont;
17+
18+
[TestInitialize]
19+
public void TestSetup()
20+
{
21+
_robotoFont = OpenTypeFonts.GetFontData(FontFolders, "Roboto", FontSubFamily.Regular);
22+
}
23+
24+
[TestMethod]
25+
public void DefaultFontProvider_EmojiGlyph_ShouldUseFallbackFont()
26+
{
27+
// Arrange
28+
var shaper = new TextShaper(_robotoFont);
29+
30+
// Act
31+
var shaped = shaper.Shape("😀");
32+
var usedFonts = shaper.GetUsedFonts().ToList();
33+
34+
// Assert
35+
Assert.AreEqual(1, shaped.Glyphs.Length, "Should have 1 glyph");
36+
Assert.AreNotEqual((ushort)0, shaped.Glyphs[0].GlyphId, "Emoji should not be .notdef");
37+
Assert.AreEqual((byte)0, shaped.Glyphs[0].FontId, "Emoji is the only font used (FontId=0)");
38+
39+
// Verify it's NOT the primary font
40+
Assert.AreEqual(1, usedFonts.Count, "Should only use one font (emoji fallback)");
41+
Assert.AreNotEqual(_robotoFont, usedFonts[0], "Should be emoji font, not Roboto");
42+
}
43+
44+
[TestMethod]
45+
public void DefaultFontProvider_LatinText_ShouldUsePrimaryFont()
46+
{
47+
// Arrange
48+
var shaper = new TextShaper(_robotoFont);
49+
50+
// Act
51+
var shaped = shaper.Shape("Hello World");
52+
53+
// Assert
54+
foreach (var glyph in shaped.Glyphs)
55+
{
56+
Assert.AreEqual((byte)0, glyph.FontId, "All glyphs should be from primary font");
57+
}
58+
}
59+
60+
[TestMethod]
61+
public void DefaultFontProvider_MixedTextAndEmoji_ShouldUseMultipleFonts()
62+
{
63+
// Arrange
64+
var shaper = new TextShaper(_robotoFont);
65+
66+
// Act
67+
var shaped = shaper.Shape("Hello 😀 World");
68+
var usedFonts = shaper.GetUsedFonts().ToList();
69+
70+
// Assert
71+
Assert.AreEqual(2, usedFonts.Count, "Should use 2 fonts (primary + emoji fallback)");
72+
Assert.AreEqual(_robotoFont, usedFonts[0], "First font should be primary");
73+
Assert.AreNotEqual(_robotoFont, usedFonts[1], "Second font should be emoji fallback");
74+
}
75+
76+
[TestMethod]
77+
public void TextShaper_SurrogatePair_ShouldMapToSingleGlyph()
78+
{
79+
// Arrange
80+
var shaper = new TextShaper(_robotoFont);
81+
string text = "😀"; // U+1F600 = 2 chars in UTF-16
82+
83+
// Act
84+
var shaped = shaper.Shape(text);
85+
86+
// Assert
87+
Assert.AreEqual(2, text.Length, "Emoji should be 2 chars in .NET");
88+
Assert.AreEqual(1, shaped.Glyphs.Length, "Should map to 1 glyph");
89+
Assert.AreEqual((byte)2, shaped.Glyphs[0].CharCount, "Glyph should span 2 chars");
90+
Assert.AreEqual((ushort)0, shaped.Glyphs[0].ClusterIndex, "Should start at char 0");
91+
}
92+
93+
[TestMethod]
94+
public void TextShaper_MultipleEmoji_ShouldMapCorrectly()
95+
{
96+
// Arrange
97+
var shaper = new TextShaper(_robotoFont);
98+
string text = "😀😁😂"; // 3 emoji = 6 chars in UTF-16
99+
100+
// Act
101+
var shaped = shaper.Shape(text);
102+
103+
// Assert
104+
Assert.AreEqual(6, text.Length, "3 emoji = 6 chars");
105+
Assert.AreEqual(3, shaped.Glyphs.Length, "Should map to 3 glyphs");
106+
107+
Assert.AreEqual((ushort)0, shaped.Glyphs[0].ClusterIndex, "First emoji at char 0");
108+
Assert.AreEqual((byte)2, shaped.Glyphs[0].CharCount, "First emoji spans 2 chars");
109+
110+
Assert.AreEqual((ushort)2, shaped.Glyphs[1].ClusterIndex, "Second emoji at char 2");
111+
Assert.AreEqual((byte)2, shaped.Glyphs[1].CharCount, "Second emoji spans 2 chars");
112+
113+
Assert.AreEqual((ushort)4, shaped.Glyphs[2].ClusterIndex, "Third emoji at char 4");
114+
Assert.AreEqual((byte)2, shaped.Glyphs[2].CharCount, "Third emoji spans 2 chars");
115+
}
116+
}
117+
}

src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public void Compare_MeasureMultiLineText_NewImplementationFixesBugs()
119119
Debug.WriteLine($"Line '{line}': {lineResult.Width}");
120120
}
121121

122-
double expectedHeight = shaper.GetLineHeightInPoints(11.0) * lines.Length;
122+
double expectedHeight = shaper.GetLineHeightInPoints(11.0f) * lines.Length;
123123

124124
// Assert
125125
Debug.WriteLine("");
@@ -164,7 +164,7 @@ public void Compare_GetSingleLineSpacing_ShouldMatch()
164164

165165
// Act
166166
var oldSpacing = oldMeasurer.GetSingleLineSpacing();
167-
var newSpacing = shaper.GetLineHeightInPoints(11.0);
167+
var newSpacing = shaper.GetLineHeightInPoints(11.0f);
168168

169169
// Assert
170170
Debug.WriteLine($"Old Spacing: {oldSpacing}, New Spacing: {newSpacing}");
@@ -185,7 +185,7 @@ public void Compare_GetBaseLine_ShouldMatch()
185185

186186
// Act
187187
var oldBaseline = oldMeasurer.GetBaseLine();
188-
var newBaseline = shaper.GetBaseLineInPoints(11.0);
188+
var newBaseline = shaper.GetBaseLineInPoints(11.0f);
189189

190190
// Assert
191191
Debug.WriteLine($"Old Baseline: {oldBaseline}, New Baseline: {newBaseline}");

src/EPPlus.Fonts.OpenType.Tests/TextShaping/MarkToBaseTests.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@ public void MarkToBaseTest()
2121
var shaper = new TextShaper(font);
2222

2323
string test = "A\u0302\u0309";
24-
var shaped = shaper.Shape(test, ShapingOptions.Full);
25-
26-
Debug.WriteLine("Glyphs för 'Ẩn ặ':");
27-
foreach (var g in shaped.Glyphs)
24+
// Lägg till lite synchronization för att verifiera
25+
lock (typeof(MarkToBaseTests))
2826
{
29-
Debug.WriteLine($"GID={g.GlyphId,-4} XAdv={g.XAdvance,-5} YOff={g.YOffset,-4}");
27+
var shaped = shaper.Shape(test, ShapingOptions.Full);
28+
29+
foreach (var g in shaped.Glyphs)
30+
{
31+
Debug.WriteLine($"GID={g.GlyphId,-4} XAdv={g.XAdvance,-5} YOff={g.YOffset,-4}");
32+
}
33+
34+
Assert.IsTrue(shaped.Glyphs.Any(x => x.YOffset > 0),
35+
$"Expected YOffset > 0. Got: {string.Join(", ", shaped.Glyphs.Select(g => $"Y={g.YOffset}"))}");
3036
}
31-
Assert.IsTrue(shaped.Glyphs.Any(x => x.YOffset > 0));
3237
}
3338
}
3439
}

src/EPPlus.Fonts.OpenType/EmbeddedFonts.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,55 @@ private static OpenTypeFont LoadCached(string resourceName)
5252
}
5353
}
5454

55+
/// <summary>
56+
/// Reads all bytes from a stream into a byte array.
57+
/// .NET 3.5 compatible (no CopyTo available).
58+
/// </summary>
5559
private static byte[] ReadStreamFully(Stream stream)
5660
{
57-
throw new NotImplementedException();
61+
if (stream == null)
62+
throw new ArgumentNullException("stream");
63+
64+
// Try to use stream length if available (e.g., MemoryStream, FileStream)
65+
if (stream.CanSeek)
66+
{
67+
long length = stream.Length - stream.Position;
68+
if (length > int.MaxValue)
69+
throw new InvalidOperationException("Stream is too large to read into memory");
70+
71+
byte[] buffer = new byte[(int)length];
72+
int offset = 0;
73+
int remaining = (int)length;
74+
75+
while (remaining > 0)
76+
{
77+
int read = stream.Read(buffer, offset, remaining);
78+
if (read <= 0)
79+
throw new EndOfStreamException("Stream ended before reading all bytes");
80+
81+
offset += read;
82+
remaining -= read;
83+
}
84+
85+
return buffer;
86+
}
87+
else
88+
{
89+
// Non-seekable stream (rare for embedded resources, but handle it)
90+
// Use MemoryStream to accumulate bytes
91+
using (var ms = new MemoryStream())
92+
{
93+
byte[] buffer = new byte[8192]; // 8KB buffer
94+
int bytesRead;
95+
96+
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
97+
{
98+
ms.Write(buffer, 0, bytesRead);
99+
}
100+
101+
return ms.ToArray();
102+
}
103+
}
58104
}
59105
}
60106
}

src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,17 @@ public TextLayoutEngine(
6363
_spaceWidthCache = new Dictionary<float, double>();
6464
}
6565

66-
public double GetLineHeightInPoints(double fontSize)
66+
public double GetLineHeightInPoints(float fontSize)
6767
{
6868
return _shaper.GetLineHeightInPoints(fontSize);
6969
}
7070

71-
public double GetBaseLineInPoints(double fontSize)
71+
public double GetBaseLineInPoints(float fontSize)
7272
{
7373
return _shaper.GetBaseLineInPoints(fontSize);
7474
}
7575

76-
public double GetDescentInPoints(double fontSize)
76+
public double GetDescentInPoints(float fontSize)
7777
{
7878
return _shaper.GetDescentInPoints(fontSize);
7979
}

0 commit comments

Comments
 (0)