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+ 02/19/2026 EPPlus Software AB Vertical subsetting tests (vhea/vmtx)
12+ *************************************************************************************************/
13+ using EPPlus . Fonts . OpenType . FontValidation ;
14+ using EPPlus . Fonts . OpenType . Tests . Helpers ;
15+
16+ namespace EPPlus . Fonts . OpenType . Tests . Subsetting
17+ {
18+ [ TestClass ]
19+ public class VerticalSubsettingTests : FontTestBase
20+ {
21+ public override TestContext TestContext { get ; set ; }
22+
23+ [ ClassInitialize ]
24+ public static void ClassInitialize ( TestContext ctx )
25+ {
26+ FontDirectoriesTestHelper . ClassInitialize ( ctx ) ;
27+ }
28+
29+ #region vhea/vmtx presence tests
30+
31+ [ TestMethod ]
32+ public void Subset_CjkFont_SubsetContainsVheaTable ( )
33+ {
34+ // Arrange
35+ var font = OpenTypeFonts . GetFontDataOpen ( FontFolders , "BIZ UDGothic" , FontSubFamily . Regular , true ) ;
36+ Assert . IsNotNull ( font . VheaTable , "BIZ UDGothic should have a vhea table" ) ;
37+
38+ // Act
39+ var subset = font . CreateSubset ( "日本語" ) ;
40+ var bytes = subset . Serialize ( ) ;
41+ var parsed = new OpenTypeFont ( bytes , font . Format ) ;
42+
43+ SaveFontForCurrentTest ( parsed ) ;
44+
45+ // Assert
46+ Assert . IsNotNull ( parsed . VheaTable , "Subset should contain vhea table" ) ;
47+ }
48+
49+ [ TestMethod ]
50+ public void Subset_CjkFont_SubsetContainsVmtxTable ( )
51+ {
52+ // Arrange
53+ var font = OpenTypeFonts . GetFontDataOpen ( FontFolders , "BIZ UDGothic" , FontSubFamily . Regular , true ) ;
54+ Assert . IsNotNull ( font . VmtxTable , "BIZ UDGothic should have a vmtx table" ) ;
55+
56+ // Act
57+ var subset = font . CreateSubset ( "日本語" ) ;
58+ var bytes = subset . Serialize ( ) ;
59+ var parsed = new OpenTypeFont ( bytes , font . Format ) ;
60+
61+ SaveFontForCurrentTest ( parsed ) ;
62+
63+ // Assert
64+ Assert . IsNotNull ( parsed . VmtxTable , "Subset should contain vmtx table" ) ;
65+ }
66+
67+ [ TestMethod ]
68+ public void Subset_FontWithoutVmtx_SubsetDoesNotContainVmtxTable ( )
69+ {
70+ // Arrange - Roboto has no vmtx/vhea
71+ var font = OpenTypeFonts . GetFontDataOpen ( FontFolders , "Roboto" , FontSubFamily . Regular , true ) ;
72+ Assert . IsNull ( font . VmtxTable , "Roboto should not have a vmtx table" ) ;
73+
74+ // Act
75+ var subset = font . CreateSubset ( "ABC" ) ;
76+ var bytes = subset . Serialize ( ) ;
77+ var parsed = new OpenTypeFont ( bytes , font . Format ) ;
78+
79+ // Assert - vmtx should not be introduced by subsetting
80+ Assert . IsNull ( parsed . VmtxTable , "Subset of font without vmtx should not contain vmtx table" ) ;
81+ }
82+
83+ #endregion
84+
85+ #region vhea correctness tests
86+
87+ [ TestMethod ]
88+ public void Subset_CjkFont_VheaNumberOfVMetricsMatchesGlyphCount ( )
89+ {
90+ // Arrange
91+ var font = OpenTypeFonts . GetFontDataOpen ( FontFolders , "BIZ UDGothic" , FontSubFamily . Regular , true ) ;
92+
93+ // Act
94+ var subset = font . CreateSubset ( "日本語" ) ;
95+ var bytes = subset . Serialize ( ) ;
96+ var parsed = new OpenTypeFont ( bytes , font . Format ) ;
97+
98+ SaveFontForCurrentTest ( parsed ) ;
99+
100+ // Assert - NumberOfVMetrics must equal numGlyphs (same simplification as hmtx)
101+ Assert . AreEqual (
102+ parsed . MaxpTable . numGlyphs ,
103+ parsed . VheaTable . NumberOfVMetrics ,
104+ "vhea.NumberOfVMetrics should equal numGlyphs in subset" ) ;
105+ }
106+
107+ #endregion
108+
109+ #region vmtx correctness tests
110+
111+ [ TestMethod ]
112+ public void Subset_CjkFont_VmtxAdvanceHeightPreservedForSubsettedGlyphs ( )
113+ {
114+ // Arrange
115+ var font = OpenTypeFonts . GetFontDataOpen ( FontFolders , "BIZ UDGothic" , FontSubFamily . Regular , true ) ;
116+ var text = "日" ;
117+
118+ // Get original glyph ID and advance height before subsetting
119+ ushort originalGlyphId ;
120+ font . CmapTable . TryGetGlyphId ( '日' , out originalGlyphId ) ;
121+ var originalAdvanceHeight = font . VmtxTable . GetAdvanceHeight ( originalGlyphId ) ;
122+
123+ // Act
124+ var subset = font . CreateSubset ( text ) ;
125+ var bytes = subset . Serialize ( ) ;
126+ var parsed = new OpenTypeFont ( bytes , font . Format ) ;
127+
128+ SaveFontForCurrentTest ( parsed ) ;
129+
130+ // Assert - resolve new glyph ID in subset and verify advance height is preserved
131+ ushort subsetGlyphId ;
132+ parsed . CmapTable . TryGetGlyphId ( '日' , out subsetGlyphId ) ;
133+ var subsetAdvanceHeight = parsed . VmtxTable . GetAdvanceHeight ( subsetGlyphId ) ;
134+
135+ Assert . AreEqual ( originalAdvanceHeight , subsetAdvanceHeight ,
136+ $ "AdvanceHeight for '日' should be preserved after subsetting " +
137+ $ "(original={ originalAdvanceHeight } , subset={ subsetAdvanceHeight } )") ;
138+ }
139+
140+ [ TestMethod ]
141+ public void Subset_CjkFont_VmtxEntryCountMatchesGlyphCount ( )
142+ {
143+ // Arrange
144+ var font = OpenTypeFonts . GetFontDataOpen ( FontFolders , "BIZ UDGothic" , FontSubFamily . Regular , true ) ;
145+
146+ // Act
147+ var subset = font . CreateSubset ( "東京" ) ;
148+ var bytes = subset . Serialize ( ) ;
149+ var parsed = new OpenTypeFont ( bytes , font . Format ) ;
150+
151+ SaveFontForCurrentTest ( parsed ) ;
152+
153+ // Assert - VMetrics count must equal numGlyphs
154+ Assert . AreEqual (
155+ parsed . MaxpTable . numGlyphs ,
156+ parsed . VmtxTable . VMetrics . Count ,
157+ "vmtx.VMetrics.Count should equal numGlyphs in subset" ) ;
158+ }
159+
160+ [ TestMethod ]
161+ public void Subset_CjkFont_PassesValidationAfterSubsetting ( )
162+ {
163+ // Arrange
164+ var font = OpenTypeFonts . GetFontDataOpen ( FontFolders , "BIZ UDGothic" , FontSubFamily . Regular , true ) ;
165+
166+ // Act
167+ var subset = font . CreateSubset ( "日本語テスト" ) ;
168+ var bytes = subset . Serialize ( ) ;
169+ var parsed = new OpenTypeFont ( bytes , font . Format ) ;
170+
171+ SaveFontForCurrentTest ( parsed ) ;
172+
173+ // Assert - full font validation should pass without errors
174+ FontTestHelper . AssertFontValid ( parsed , FontValidationSeverity . Warning ) ;
175+ }
176+
177+ #endregion
178+ }
179+ }
0 commit comments