@@ -41,6 +41,7 @@ internal class TrueTypeInterpreter
4141 private readonly ExecutionStack stack ;
4242 private readonly InstructionStream [ ] functions ;
4343 private readonly InstructionStream [ ] instructionDefs ;
44+ private float [ ] baseControlValueTable ;
4445 private float [ ] controlValueTable ;
4546 private readonly int [ ] storage ;
4647 private IReadOnlyList < ushort > contours ;
@@ -66,7 +67,9 @@ internal class TrueTypeInterpreter
6667 private const int MaxCallStack = 128 ;
6768 private const float Epsilon = 0.000001F ;
6869
69- private readonly List < OpCode > debugList = new ( ) ;
70+ #if DEBUG
71+ private readonly List < OpCode > debugList = [ ] ;
72+ #endif
7073
7174 public TrueTypeInterpreter ( int maxStack , int maxStorage , int maxFunctions , int maxInstructionDefs , int maxTwilightPoints )
7275 {
@@ -77,8 +80,9 @@ public TrueTypeInterpreter(int maxStack, int maxStorage, int maxFunctions, int m
7780 this . state = default ;
7881 this . cvtState = default ;
7982 this . twilight = new Zone ( maxTwilightPoints , isTwilight : true ) ;
80- this . controlValueTable = Array . Empty < float > ( ) ;
81- this . contours = Array . Empty < ushort > ( ) ;
83+ this . controlValueTable = [ ] ;
84+ this . baseControlValueTable = [ ] ;
85+ this . contours = [ ] ;
8286 }
8387
8488 public void InitializeFunctionDefs ( byte [ ] instructions )
@@ -97,7 +101,6 @@ public void SetControlValueTable(short[]? cvt, float scale, float ppem, byte[]?
97101 this . controlValueTable = new float [ cvt . Length ] ;
98102 }
99103
100- // TODO: How about SIMD here? Will the JIT vectorize this?
101104 for ( int i = 0 ; i < cvt . Length ; i ++ )
102105 {
103106 this . controlValueTable [ i ] = cvt [ i ] * scale ;
@@ -130,51 +133,141 @@ public void SetControlValueTable(short[]? cvt, float scale, float ppem, byte[]?
130133 this . cvtState . Loop = 1 ;
131134 }
132135 }
136+
137+ if ( this . controlValueTable . Length > 0 )
138+ {
139+ if ( this . baseControlValueTable . Length != this . controlValueTable . Length )
140+ {
141+ this . baseControlValueTable = new float [ this . controlValueTable . Length ] ;
142+ }
143+
144+ Array . Copy ( this . controlValueTable , this . baseControlValueTable , this . controlValueTable . Length ) ;
145+ }
146+ else
147+ {
148+ this . baseControlValueTable = [ ] ;
149+ }
133150 }
134151
135- public void HintGlyph (
152+ /// <summary>
153+ /// Attempts to apply TrueType hinting instructions to the specified glyph outline.
154+ /// </summary>
155+ /// <remarks>
156+ /// Hinting will not be applied if the instructions buffer is empty or if grid fitting is
157+ /// inhibited by the current interpreter state. If the instructions are malformed or an error occurs during
158+ /// execution, the method returns <see langword="false"/> and the glyph outline remains unhinted.
159+ /// </remarks>
160+ /// <param name="controlPoints">An array of control points representing the glyph's outline to be hinted.</param>
161+ /// <param name="endPoints">A read-only list of indices indicating the end points of each contour in the glyph.</param>
162+ /// <param name="instructions">A read-only memory buffer containing the TrueType hinting instructions to execute.</param>
163+ /// <param name="isComposite">Indicates whether the glyph is a composite glyph. Set to <see langword="true"/> for composite glyphs; otherwise, <see langword="false"/>.</param>
164+ /// <returns><see langword="true"/> if hinting was successfully applied; otherwise, <see langword="false"/>.</returns>
165+ public bool TryHintGlyph (
136166 ControlPoint [ ] controlPoints ,
137167 IReadOnlyList < ushort > endPoints ,
138168 ReadOnlyMemory < byte > instructions ,
139169 bool isComposite )
140170 {
141171 if ( instructions . Length == 0 )
142172 {
143- return ;
173+ return false ;
144174 }
145175
146- // check if the CVT program disabled hinting
176+ // Check if the CVT program disabled hinting
147177 if ( ( this . state . InstructionControl & InstructionControlFlags . InhibitGridFitting ) != 0 )
148178 {
149- return ;
179+ return false ;
150180 }
151181
152- // save contours and points
153- this . contours = endPoints ;
154- this . zp0 = this . zp1 = this . zp2 = this . points = new Zone ( controlPoints , isTwilight : false ) ;
182+ try
183+ {
184+ // Save contours and points
185+ this . contours = endPoints ;
186+ this . zp0 = this . zp1 = this . zp2 = this . points = new Zone ( controlPoints , isTwilight : false ) ;
155187
156- // reset all of our shared state
157- this . state = this . cvtState ;
158- this . callStackSize = 0 ;
159- this . debugList . Clear ( ) ;
160- this . stack . Clear ( ) ;
161- this . OnVectorsUpdated ( ) ;
162- this . iupXCalled = false ;
163- this . iupYCalled = false ;
164- this . isComposite = isComposite ;
188+ // reset all of our shared state
189+ this . state = this . cvtState ;
190+ this . callStackSize = 0 ;
165191
166- // normalize the round state settings
167- switch ( this . state . RoundState )
192+ // FreeType's interpreter treats the storage area and glyph-level CVT modifications as non-persistent.
193+ // Reset storage and restore the baseline CVT state for each glyph.
194+ Array . Clear ( this . storage , 0 , this . storage . Length ) ;
195+
196+ if ( this . baseControlValueTable . Length > 0 )
197+ {
198+ if ( this . controlValueTable . Length != this . baseControlValueTable . Length )
199+ {
200+ this . controlValueTable = new float [ this . baseControlValueTable . Length ] ;
201+ }
202+
203+ Array . Copy ( this . baseControlValueTable , this . controlValueTable , this . baseControlValueTable . Length ) ;
204+ }
205+ else
206+ {
207+ this . controlValueTable = [ ] ;
208+ }
209+
210+ this . ResetTwilightZone ( ) ;
211+
212+ #if DEBUG
213+ this . debugList . Clear ( ) ;
214+ #endif
215+
216+ this . stack . Clear ( ) ;
217+ this . OnVectorsUpdated ( ) ;
218+ this . iupXCalled = false ;
219+ this . iupYCalled = false ;
220+ this . isComposite = isComposite ;
221+
222+ // normalize the round state settings
223+ switch ( this . state . RoundState )
224+ {
225+ case RoundMode . Super :
226+ this . SetSuperRound ( 1.0f ) ;
227+ break ;
228+ case RoundMode . Super45 :
229+ this . SetSuperRound ( Sqrt2Over2 ) ;
230+ break ;
231+ }
232+
233+ this . Execute ( new StackInstructionStream ( instructions , 0 ) , false , false ) ;
234+ return true ;
235+ }
236+ catch ( Exception )
168237 {
169- case RoundMode . Super :
170- this . SetSuperRound ( 1.0f ) ;
171- break ;
172- case RoundMode . Super45 :
173- this . SetSuperRound ( Sqrt2Over2 ) ;
174- break ;
238+ // The interpreter can fail for malformed instructions; in that case we skip hinting.
239+ Array . Clear ( this . points . TouchState , 0 , this . points . TouchState . Length ) ;
240+
241+ // Reset interpreter state so nothing leaks if the caller catches.
242+ this . stack . Clear ( ) ;
243+ this . callStackSize = 0 ;
244+ this . contours = [ ] ;
245+ this . zp0 = this . zp1 = this . zp2 = this . points = default ;
246+
247+ this . state = this . cvtState ;
248+ this . OnVectorsUpdated ( ) ;
249+ this . iupXCalled = false ;
250+ this . iupYCalled = false ;
251+ this . isComposite = false ;
252+ return false ;
175253 }
254+ }
176255
177- this . Execute ( new StackInstructionStream ( instructions , 0 ) , false , false ) ;
256+ private void ResetTwilightZone ( )
257+ {
258+ // In FreeType, twilight points are defined to have original coordinates at (0,0).
259+ // Reset both original and current coordinates, and clear touch state, to avoid state leaking between glyphs.
260+ ControlPoint [ ] twCurrent = this . twilight . Current ;
261+ ControlPoint [ ] twOriginal = this . twilight . Original ;
262+
263+ int len = twCurrent . Length ;
264+ for ( int i = 0 ; i < len ; i ++ )
265+ {
266+ twCurrent [ i ] . Point = default ;
267+ twOriginal [ i ] . Point = default ;
268+ }
269+
270+ Array . Clear ( this . twilight . TouchState , 0 , this . twilight . TouchState . Length ) ;
178271 }
179272
180273 private void Execute ( StackInstructionStream stream , bool inFunction , bool allowFunctionDefs )
@@ -183,7 +276,10 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
183276 while ( ! stream . Done )
184277 {
185278 OpCode opcode = stream . NextOpCode ( ) ;
279+
280+ #if DEBUG
186281 this . debugList . Add ( opcode ) ;
282+ #endif
187283 switch ( opcode )
188284 {
189285 // ==== PUSH INSTRUCTIONS ====
@@ -316,7 +412,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
316412 {
317413 int y = this . stack . Pop ( ) ;
318414 int x = this . stack . Pop ( ) ;
319- var vec = Vector2 . Normalize ( new Vector2 ( F2Dot14ToFloat ( x ) , F2Dot14ToFloat ( y ) ) ) ;
415+ Vector2 vec = Vector2 . Normalize ( new Vector2 ( F2Dot14ToFloat ( x ) , F2Dot14ToFloat ( y ) ) ) ;
320416 if ( opcode == OpCode . SFVFS )
321417 {
322418 this . state . Freedom = vec ;
@@ -653,7 +749,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
653749 case OpCode . SHC1 :
654750 {
655751 Vector2 displacement = this . ComputeDisplacement ( ( int ) opcode , out Zone zone , out int point ) ;
656- TouchState touch = this . GetTouchState ( ) ;
752+
657753 int contour = this . stack . Pop ( ) ;
658754 int start = contour == 0 ? 0 : this . contours [ contour - 1 ] + 1 ;
659755 int count = this . zp2 . IsTwilight ? this . zp2 . Current . Length : this . contours [ contour ] + 1 ;
@@ -665,8 +761,8 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
665761 // Don't move the reference point
666762 if ( zone . Current != current || point != i )
667763 {
668- current [ i ] . Point += displacement ;
669- states [ i ] |= touch ;
764+ current [ i ] . Point . Y += displacement . Y ;
765+ states [ i ] |= TouchState . Y ;
670766 }
671767 }
672768 }
@@ -692,7 +788,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
692788 // Don't move the reference point
693789 if ( zone . Current != current || point != i )
694790 {
695- current [ i ] . Point += displacement ;
791+ current [ i ] . Point . Y += displacement . Y ;
696792 }
697793 }
698794 }
@@ -1850,20 +1946,18 @@ private void ShiftPoints(Vector2 displacement)
18501946
18511947 private void MovePoint ( Zone zone , int index , float distance )
18521948 {
1853- if ( this . isComposite )
1854- {
1855- Vector2 point = zone . GetCurrent ( index ) + ( distance * this . state . Freedom / this . fdotp ) ;
1856- TouchState touch = this . GetTouchState ( ) ;
1857- zone . Current [ index ] . Point = point ;
1858- zone . TouchState [ index ] |= touch ;
1859- }
1860- else
1949+ // Copy FreeType Interpreter V40 and ignore instructions on the x-axis.
1950+ // This increases resolution on the x-axis and prevents glyph explosions on legacy fonts.
1951+ // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298
1952+ Vector2 cur = zone . GetCurrent ( index ) ;
1953+
1954+ // V40: ignore x movement, apply only the Y component.
1955+ float dy = distance * this . state . Freedom . Y / this . fdotp ;
1956+
1957+ // Only mark Y as touched if Y actually changed.
1958+ if ( dy != 0F )
18611959 {
1862- // Copy FreeType Interpreter V40 and ignore instructions on the x-axis.
1863- // This increases resolution on the x-axis and prevents glyph explosions on legacy fonts.
1864- // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298
1865- Vector2 point = zone . GetCurrent ( index ) + ( distance * this . state . Freedom / this . fdotp ) ;
1866- zone . Current [ index ] . Point . Y = point . Y ;
1960+ zone . Current [ index ] . Point . Y = cur . Y + dy ;
18671961 zone . TouchState [ index ] |= TouchState . Y ;
18681962 }
18691963 }
@@ -2474,7 +2568,7 @@ public Zone(ControlPoint[] controlPoints, bool isTwilight)
24742568 this . IsTwilight = isTwilight ;
24752569 this . Current = controlPoints ;
24762570
2477- var original = new ControlPoint [ controlPoints . Length ] ;
2571+ ControlPoint [ ] original = new ControlPoint [ controlPoints . Length ] ;
24782572 controlPoints . AsSpan ( ) . CopyTo ( original ) ;
24792573 this . Original = original ;
24802574 this . TouchState = new TouchState [ controlPoints . Length ] ;
0 commit comments