@@ -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
70+ #if DEBUG
6971 private readonly List < OpCode > debugList = [ ] ;
72+ #endif
7073
7174 public TrueTypeInterpreter ( int maxStack , int maxStorage , int maxFunctions , int maxInstructionDefs , int maxTwilightPoints )
7275 {
@@ -78,6 +81,7 @@ public TrueTypeInterpreter(int maxStack, int maxStorage, int maxFunctions, int m
7881 this . cvtState = default ;
7982 this . twilight = new Zone ( maxTwilightPoints , isTwilight : true ) ;
8083 this . controlValueTable = [ ] ;
84+ this . baseControlValueTable = [ ] ;
8185 this . contours = Array . Empty < ushort > ( ) ;
8286 }
8387
@@ -129,51 +133,142 @@ public void SetControlValueTable(short[]? cvt, float scale, float ppem, byte[]?
129133 this . cvtState . Loop = 1 ;
130134 }
131135 }
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+ }
132150 }
133151
134- 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 (
135166 ControlPoint [ ] controlPoints ,
136167 IReadOnlyList < ushort > endPoints ,
137168 ReadOnlyMemory < byte > instructions ,
138169 bool isComposite )
139170 {
140171 if ( instructions . Length == 0 )
141172 {
142- return ;
173+ return false ;
143174 }
144175
145- // check if the CVT program disabled hinting
176+ // Check if the CVT program disabled hinting
146177 if ( ( this . state . InstructionControl & InstructionControlFlags . InhibitGridFitting ) != 0 )
147178 {
148- return ;
179+ return false ;
149180 }
150181
151- // save contours and points
152- this . contours = endPoints ;
153- 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 ) ;
187+
188+ // reset all of our shared state
189+ this . state = this . cvtState ;
190+ this . callStackSize = 0 ;
154191
155- // reset all of our shared state
156- this . state = this . cvtState ;
157- this . callStackSize = 0 ;
158- this . debugList . Clear ( ) ;
159- this . stack . Clear ( ) ;
160- this . OnVectorsUpdated ( ) ;
161- this . iupXCalled = false ;
162- this . iupYCalled = false ;
163- this . isComposite = isComposite ;
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 ) ;
164195
165- // normalize the round state settings
166- switch ( this . state . RoundState )
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 )
167237 {
168- case RoundMode . Super :
169- this . SetSuperRound ( 1.0f ) ;
170- break ;
171- case RoundMode . Super45 :
172- this . SetSuperRound ( Sqrt2Over2 ) ;
173- break ;
238+ // TODO: Is there a general Reset I can call?
239+ // The interpreter can fail for malformed instructions; in that case we skip hinting.
240+ Array . Clear ( this . points . TouchState , 0 , this . points . TouchState . Length ) ;
241+
242+ // Reset interpreter state so nothing leaks if the caller catches.
243+ this . stack . Clear ( ) ;
244+ this . callStackSize = 0 ;
245+ this . contours = Array . Empty < ushort > ( ) ;
246+ this . zp0 = this . zp1 = this . zp2 = this . points = default ;
247+
248+ this . state = this . cvtState ;
249+ this . OnVectorsUpdated ( ) ;
250+ this . iupXCalled = false ;
251+ this . iupYCalled = false ;
252+ this . isComposite = false ;
253+ return false ;
254+ }
255+ }
256+
257+ private void ResetTwilightZone ( )
258+ {
259+ // In FreeType, twilight points are defined to have original coordinates at (0,0).
260+ // Reset both original and current coordinates, and clear touch state, to avoid state leaking between glyphs.
261+ ControlPoint [ ] twCurrent = this . twilight . Current ;
262+ ControlPoint [ ] twOriginal = this . twilight . Original ;
263+
264+ int len = twCurrent . Length ;
265+ for ( int i = 0 ; i < len ; i ++ )
266+ {
267+ twCurrent [ i ] . Point = default ;
268+ twOriginal [ i ] . Point = default ;
174269 }
175270
176- this . Execute ( new StackInstructionStream ( instructions , 0 ) , false , false ) ;
271+ Array . Clear ( this . twilight . TouchState ) ;
177272 }
178273
179274 private void Execute ( StackInstructionStream stream , bool inFunction , bool allowFunctionDefs )
@@ -182,7 +277,10 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
182277 while ( ! stream . Done )
183278 {
184279 OpCode opcode = stream . NextOpCode ( ) ;
280+
281+ #if DEBUG
185282 this . debugList . Add ( opcode ) ;
283+ #endif
186284 switch ( opcode )
187285 {
188286 // ==== PUSH INSTRUCTIONS ====
@@ -226,19 +324,16 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
226324 // ==== STORAGE MANAGEMENT ====
227325 case OpCode . RS :
228326 {
229- int loc = this . stack . Pop ( ) ;
230- this . stack . Push ( ( uint ) loc < ( uint ) this . storage . Length ? this . storage [ loc ] : 0 ) ;
327+ int loc = CheckIndex ( this . stack . Pop ( ) , this . storage . Length ) ;
328+ this . stack . Push ( this . storage [ loc ] ) ;
231329 }
232330
233331 break ;
234332 case OpCode . WS :
235333 {
236334 int value = this . stack . Pop ( ) ;
237- int loc = this . stack . Pop ( ) ;
238- if ( ( uint ) loc < ( uint ) this . storage . Length )
239- {
240- this . storage [ loc ] = value ;
241- }
335+ int loc = CheckIndex ( this . stack . Pop ( ) , this . storage . Length ) ;
336+ this . storage [ loc ] = value ;
242337 }
243338
244339 break ;
@@ -247,22 +342,16 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
247342 case OpCode . WCVTP :
248343 {
249344 float value = this . stack . PopFloat ( ) ;
250- int loc = this . stack . Pop ( ) ;
251- if ( ( uint ) loc < ( uint ) this . controlValueTable . Length )
252- {
253- this . controlValueTable [ loc ] = value ;
254- }
345+ int loc = CheckIndex ( this . stack . Pop ( ) , this . controlValueTable . Length ) ;
346+ this . controlValueTable [ loc ] = value ;
255347 }
256348
257349 break ;
258350 case OpCode . WCVTF :
259351 {
260352 int value = this . stack . Pop ( ) ;
261- int loc = this . stack . Pop ( ) ;
262- if ( ( uint ) loc < ( uint ) this . controlValueTable . Length )
263- {
264- this . controlValueTable [ loc ] = value * this . scale ;
265- }
353+ int loc = CheckIndex ( this . stack . Pop ( ) , this . controlValueTable . Length ) ;
354+ this . controlValueTable [ loc ] = value * this . scale ;
266355 }
267356
268357 break ;
@@ -661,7 +750,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
661750 case OpCode . SHC1 :
662751 {
663752 Vector2 displacement = this . ComputeDisplacement ( ( int ) opcode , out Zone zone , out int point ) ;
664- TouchState touch = this . GetTouchState ( ) ;
753+
665754 int contour = this . stack . Pop ( ) ;
666755 int start = contour == 0 ? 0 : this . contours [ contour - 1 ] + 1 ;
667756 int count = this . zp2 . IsTwilight ? this . zp2 . Current . Length : this . contours [ contour ] + 1 ;
@@ -673,8 +762,8 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
673762 // Don't move the reference point
674763 if ( zone . Current != current || point != i )
675764 {
676- current [ i ] . Point += displacement ;
677- states [ i ] |= touch ;
765+ current [ i ] . Point . Y += displacement . Y ;
766+ states [ i ] |= TouchState . Y ;
678767 }
679768 }
680769 }
@@ -700,7 +789,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
700789 // Don't move the reference point
701790 if ( zone . Current != current || point != i )
702791 {
703- current [ i ] . Point += displacement ;
792+ current [ i ] . Point . Y += displacement . Y ;
704793 }
705794 }
706795 }
@@ -1387,10 +1476,8 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
13871476 amount *= 1 << ( 6 - this . state . DeltaShift ) ;
13881477
13891478 // update the CVT
1390- if ( ( uint ) cvtIndex < ( uint ) this . controlValueTable . Length )
1391- {
1392- this . controlValueTable [ cvtIndex ] += F26Dot6ToFloat ( amount ) ;
1393- }
1479+ CheckIndex ( cvtIndex , this . controlValueTable . Length ) ;
1480+ this . controlValueTable [ cvtIndex ] += F26Dot6ToFloat ( amount ) ;
13941481 }
13951482 }
13961483 }
@@ -1514,14 +1601,14 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
15141601 }
15151602 }
15161603
1517- private float ReadCvt ( )
1604+ private static int CheckIndex ( int index , int length )
15181605 {
1519- int index = this . stack . Pop ( ) ;
1520- return ( uint ) index < ( uint ) this . controlValueTable . Length
1521- ? this . controlValueTable [ index ]
1522- : 0F ;
1606+ Guard . MustBeBetweenOrEqualTo ( index , 0 , length - 1 , nameof ( index ) ) ;
1607+ return index ;
15231608 }
15241609
1610+ private float ReadCvt ( ) => this . controlValueTable [ CheckIndex ( this . stack . Pop ( ) , this . controlValueTable . Length ) ] ;
1611+
15251612 private void OnVectorsUpdated ( )
15261613 {
15271614 this . fdotp = Vector2 . Dot ( this . state . Freedom , this . state . Projection ) ;
@@ -1860,20 +1947,18 @@ private void ShiftPoints(Vector2 displacement)
18601947
18611948 private void MovePoint ( Zone zone , int index , float distance )
18621949 {
1863- if ( this . isComposite )
1864- {
1865- Vector2 point = zone . GetCurrent ( index ) + ( distance * this . state . Freedom / this . fdotp ) ;
1866- TouchState touch = this . GetTouchState ( ) ;
1867- zone . Current [ index ] . Point = point ;
1868- zone . TouchState [ index ] |= touch ;
1869- }
1870- else
1950+ // Copy FreeType Interpreter V40 and ignore instructions on the x-axis.
1951+ // This increases resolution on the x-axis and prevents glyph explosions on legacy fonts.
1952+ // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298
1953+ Vector2 cur = zone . GetCurrent ( index ) ;
1954+
1955+ // V40: ignore x movement, apply only the Y component.
1956+ float dy = distance * this . state . Freedom . Y / this . fdotp ;
1957+
1958+ // Only mark Y as touched if Y actually changed.
1959+ if ( dy != 0F )
18711960 {
1872- // Copy FreeType Interpreter V40 and ignore instructions on the x-axis.
1873- // This increases resolution on the x-axis and prevents glyph explosions on legacy fonts.
1874- // https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298
1875- Vector2 point = zone . GetCurrent ( index ) + ( distance * this . state . Freedom / this . fdotp ) ;
1876- zone . Current [ index ] . Point . Y = point . Y ;
1961+ zone . Current [ index ] . Point . Y = cur . Y + dy ;
18771962 zone . TouchState [ index ] |= TouchState . Y ;
18781963 }
18791964 }
0 commit comments