Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/SixLabors.Fonts/Tables/TrueType/Glyphs/GlyphVector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,12 @@ public static void Hint(
controlPoints[i] = glyph.ControlPoints[i];
}

interpreter.HintGlyph(controlPoints, glyph.EndPoints, glyph.Instructions, glyph.IsComposite);

for (int i = 0; i < glyph.ControlPoints.Count; i++)
if (interpreter.TryHintGlyph(controlPoints, glyph.EndPoints, glyph.Instructions, glyph.IsComposite))
{
glyph.ControlPoints[i] = controlPoints[i];
for (int i = 0; i < glyph.ControlPoints.Count; i++)
{
glyph.ControlPoints[i] = controlPoints[i];
}
}
}

Expand Down
189 changes: 142 additions & 47 deletions src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ internal class TrueTypeInterpreter
private readonly ExecutionStack stack;
private readonly InstructionStream[] functions;
private readonly InstructionStream[] instructionDefs;
private float[] baseControlValueTable;
private float[] controlValueTable;
private readonly int[] storage;
private IReadOnlyList<ushort> contours;
Expand All @@ -66,7 +67,9 @@ internal class TrueTypeInterpreter
private const int MaxCallStack = 128;
private const float Epsilon = 0.000001F;

private readonly List<OpCode> debugList = new();
#if DEBUG
private readonly List<OpCode> debugList = [];
#endif

public TrueTypeInterpreter(int maxStack, int maxStorage, int maxFunctions, int maxInstructionDefs, int maxTwilightPoints)
{
Expand All @@ -77,7 +80,8 @@ public TrueTypeInterpreter(int maxStack, int maxStorage, int maxFunctions, int m
this.state = default;
this.cvtState = default;
this.twilight = new Zone(maxTwilightPoints, isTwilight: true);
this.controlValueTable = Array.Empty<float>();
this.controlValueTable = [];
this.baseControlValueTable = [];
this.contours = Array.Empty<ushort>();
}

Expand All @@ -97,7 +101,6 @@ public void SetControlValueTable(short[]? cvt, float scale, float ppem, byte[]?
this.controlValueTable = new float[cvt.Length];
}

// TODO: How about SIMD here? Will the JIT vectorize this?
for (int i = 0; i < cvt.Length; i++)
{
this.controlValueTable[i] = cvt[i] * scale;
Expand Down Expand Up @@ -130,51 +133,142 @@ public void SetControlValueTable(short[]? cvt, float scale, float ppem, byte[]?
this.cvtState.Loop = 1;
}
}

if (this.controlValueTable.Length > 0)
{
if (this.baseControlValueTable.Length != this.controlValueTable.Length)
{
this.baseControlValueTable = new float[this.controlValueTable.Length];
}

Array.Copy(this.controlValueTable, this.baseControlValueTable, this.controlValueTable.Length);
}
else
{
this.baseControlValueTable = [];
}
}

public void HintGlyph(
/// <summary>
/// Attempts to apply TrueType hinting instructions to the specified glyph outline.
/// </summary>
/// <remarks>
/// Hinting will not be applied if the instructions buffer is empty or if grid fitting is
/// inhibited by the current interpreter state. If the instructions are malformed or an error occurs during
/// execution, the method returns <see langword="false"/> and the glyph outline remains unhinted.
/// </remarks>
/// <param name="controlPoints">An array of control points representing the glyph's outline to be hinted.</param>
/// <param name="endPoints">A read-only list of indices indicating the end points of each contour in the glyph.</param>
/// <param name="instructions">A read-only memory buffer containing the TrueType hinting instructions to execute.</param>
/// <param name="isComposite">Indicates whether the glyph is a composite glyph. Set to <see langword="true"/> for composite glyphs; otherwise, <see langword="false"/>.</param>
/// <returns><see langword="true"/> if hinting was successfully applied; otherwise, <see langword="false"/>.</returns>
public bool TryHintGlyph(
ControlPoint[] controlPoints,
IReadOnlyList<ushort> endPoints,
ReadOnlyMemory<byte> instructions,
bool isComposite)
{
if (instructions.Length == 0)
{
return;
return false;
}

// check if the CVT program disabled hinting
// Check if the CVT program disabled hinting
if ((this.state.InstructionControl & InstructionControlFlags.InhibitGridFitting) != 0)
{
return;
return false;
}

// save contours and points
this.contours = endPoints;
this.zp0 = this.zp1 = this.zp2 = this.points = new Zone(controlPoints, isTwilight: false);
try
{
// Save contours and points
this.contours = endPoints;
this.zp0 = this.zp1 = this.zp2 = this.points = new Zone(controlPoints, isTwilight: false);

// reset all of our shared state
this.state = this.cvtState;
this.callStackSize = 0;
this.debugList.Clear();
this.stack.Clear();
this.OnVectorsUpdated();
this.iupXCalled = false;
this.iupYCalled = false;
this.isComposite = isComposite;
// reset all of our shared state
this.state = this.cvtState;
this.callStackSize = 0;

// normalize the round state settings
switch (this.state.RoundState)
// FreeType's interpreter treats the storage area and glyph-level CVT modifications as non-persistent.
// Reset storage and restore the baseline CVT state for each glyph.
Array.Clear(this.storage);
Comment thread
JimBobSquarePants marked this conversation as resolved.
Outdated

if (this.baseControlValueTable.Length > 0)
{
if (this.controlValueTable.Length != this.baseControlValueTable.Length)
{
this.controlValueTable = new float[this.baseControlValueTable.Length];
}

Array.Copy(this.baseControlValueTable, this.controlValueTable, this.baseControlValueTable.Length);
}
else
{
this.controlValueTable = [];
}

this.ResetTwilightZone();

#if DEBUG
this.debugList.Clear();
#endif

this.stack.Clear();
this.OnVectorsUpdated();
this.iupXCalled = false;
this.iupYCalled = false;
this.isComposite = isComposite;

// normalize the round state settings
switch (this.state.RoundState)
{
case RoundMode.Super:
this.SetSuperRound(1.0f);
break;
case RoundMode.Super45:
this.SetSuperRound(Sqrt2Over2);
break;
}

this.Execute(new StackInstructionStream(instructions, 0), false, false);
return true;
}
catch (Exception)
{
case RoundMode.Super:
this.SetSuperRound(1.0f);
break;
case RoundMode.Super45:
this.SetSuperRound(Sqrt2Over2);
break;
// TODO: Is there a general Reset I can call?
// The interpreter can fail for malformed instructions; in that case we skip hinting.
Array.Clear(this.points.TouchState, 0, this.points.TouchState.Length);

// Reset interpreter state so nothing leaks if the caller catches.
this.stack.Clear();
this.callStackSize = 0;
this.contours = Array.Empty<ushort>();
this.zp0 = this.zp1 = this.zp2 = this.points = default;

this.state = this.cvtState;
this.OnVectorsUpdated();
this.iupXCalled = false;
this.iupYCalled = false;
this.isComposite = false;
return false;
}
}

this.Execute(new StackInstructionStream(instructions, 0), false, false);
private void ResetTwilightZone()
{
// In FreeType, twilight points are defined to have original coordinates at (0,0).
// Reset both original and current coordinates, and clear touch state, to avoid state leaking between glyphs.
ControlPoint[] twCurrent = this.twilight.Current;
ControlPoint[] twOriginal = this.twilight.Original;

int len = twCurrent.Length;
for (int i = 0; i < len; i++)
{
twCurrent[i].Point = default;
twOriginal[i].Point = default;
}

Array.Clear(this.twilight.TouchState);
Comment thread
JimBobSquarePants marked this conversation as resolved.
Outdated
}

private void Execute(StackInstructionStream stream, bool inFunction, bool allowFunctionDefs)
Expand All @@ -183,7 +277,10 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
while (!stream.Done)
{
OpCode opcode = stream.NextOpCode();

#if DEBUG
this.debugList.Add(opcode);
#endif
switch (opcode)
{
// ==== PUSH INSTRUCTIONS ====
Expand Down Expand Up @@ -316,7 +413,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
{
int y = this.stack.Pop();
int x = this.stack.Pop();
var vec = Vector2.Normalize(new Vector2(F2Dot14ToFloat(x), F2Dot14ToFloat(y)));
Vector2 vec = Vector2.Normalize(new Vector2(F2Dot14ToFloat(x), F2Dot14ToFloat(y)));
if (opcode == OpCode.SFVFS)
{
this.state.Freedom = vec;
Expand Down Expand Up @@ -653,7 +750,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
case OpCode.SHC1:
{
Vector2 displacement = this.ComputeDisplacement((int)opcode, out Zone zone, out int point);
TouchState touch = this.GetTouchState();

int contour = this.stack.Pop();
int start = contour == 0 ? 0 : this.contours[contour - 1] + 1;
int count = this.zp2.IsTwilight ? this.zp2.Current.Length : this.contours[contour] + 1;
Expand All @@ -665,8 +762,8 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
// Don't move the reference point
if (zone.Current != current || point != i)
{
current[i].Point += displacement;
states[i] |= touch;
current[i].Point.Y += displacement.Y;
states[i] |= TouchState.Y;
}
}
}
Expand All @@ -692,7 +789,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF
// Don't move the reference point
if (zone.Current != current || point != i)
{
current[i].Point += displacement;
current[i].Point.Y += displacement.Y;
}
}
}
Expand Down Expand Up @@ -1850,20 +1947,18 @@ private void ShiftPoints(Vector2 displacement)

private void MovePoint(Zone zone, int index, float distance)
{
if (this.isComposite)
{
Vector2 point = zone.GetCurrent(index) + (distance * this.state.Freedom / this.fdotp);
TouchState touch = this.GetTouchState();
zone.Current[index].Point = point;
zone.TouchState[index] |= touch;
}
else
// Copy FreeType Interpreter V40 and ignore instructions on the x-axis.
// This increases resolution on the x-axis and prevents glyph explosions on legacy fonts.
// https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298
Vector2 cur = zone.GetCurrent(index);

// V40: ignore x movement, apply only the Y component.
float dy = distance * this.state.Freedom.Y / this.fdotp;

// Only mark Y as touched if Y actually changed.
if (dy != 0F)
Comment thread
JimBobSquarePants marked this conversation as resolved.
{
// Copy FreeType Interpreter V40 and ignore instructions on the x-axis.
// This increases resolution on the x-axis and prevents glyph explosions on legacy fonts.
// https://github.com/freetype/freetype/blob/3ab1875cd22536b3d715b3b104b7fb744b9c25c5/src/truetype/ttinterp.h#L298
Vector2 point = zone.GetCurrent(index) + (distance * this.state.Freedom / this.fdotp);
zone.Current[index].Point.Y = point.Y;
zone.Current[index].Point.Y = cur.Y + dy;
zone.TouchState[index] |= TouchState.Y;
}
}
Expand Down Expand Up @@ -2474,7 +2569,7 @@ public Zone(ControlPoint[] controlPoints, bool isTwilight)
this.IsTwilight = isTwilight;
this.Current = controlPoints;

var original = new ControlPoint[controlPoints.Length];
ControlPoint[] original = new ControlPoint[controlPoints.Length];
controlPoints.AsSpan().CopyTo(original);
this.Original = original;
this.TouchState = new TouchState[controlPoints.Length];
Expand Down
Loading
Loading