Skip to content

Commit e13d255

Browse files
Use a combination of exception handling and compatibility lists for tricky fonts
1 parent 28ca2e0 commit e13d255

File tree

11 files changed

+284
-87
lines changed

11 files changed

+284
-87
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: 153 additions & 68 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

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

Comments
 (0)