Skip to content

Commit 9a33f1d

Browse files
Merge pull request #496 from SixLabors/js/fix-493
Fix pedantic TrueType interpreter behaviour for out-of-bounds CVT and storage access
2 parents 5ff7825 + a9d3b67 commit 9a33f1d

15 files changed

+344
-60
lines changed

src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphVector.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,12 @@ public static void Hint(
9292
controlPoints[i] = glyph.ControlPoints[i];
9393
}
9494

95-
interpreter.HintGlyph(controlPoints, glyph.EndPoints, glyph.Instructions, glyph.IsComposite);
96-
97-
for (int i = 0; i < glyph.ControlPoints.Count; i++)
95+
if (interpreter.TryHintGlyph(controlPoints, glyph.EndPoints, glyph.Instructions, glyph.IsComposite))
9896
{
99-
glyph.ControlPoints[i] = controlPoints[i];
97+
for (int i = 0; i < glyph.ControlPoints.Count; i++)
98+
{
99+
glyph.ControlPoints[i] = controlPoints[i];
100+
}
100101
}
101102
}
102103

src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs

Lines changed: 142 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)