// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tables.AdvancedTypographic.Variations; namespace SixLabors.Fonts.Tables.Cff { /// /// Decodes the commands and numbers making up a Type 2 CharString. A Type 2 CharString extends on the Type 1 CharString format. /// Compared to the Type 1 format, the Type 2 encoding offers smaller size and an opportunity for better rendering quality and /// performance. The Type 2 charstring operators are (with one exception) a superset of the Type 1 operators. /// /// /// A Type 2 charstring program is a sequence of unsigned 8-bit bytes that encode numbers and operators. /// The byte value specifies a operator, a number, or subsequent bytes that are to be interpreted in a specific manner. /// internal ref struct CffEvaluationEngine { private static readonly Random Random = new(); private float? width; private int nStems; private float x; private float y; private RefStack stack; private readonly ReadOnlySpan charStrings; private readonly ReadOnlySpan globalSubrBuffers; private readonly ReadOnlySpan localSubrBuffers; private TransformingGlyphRenderer transforming; private readonly int nominalWidthX; private readonly int globalBias; private readonly int localBias; private readonly Dictionary trans; private bool isDisposed; private readonly int version; private readonly GlyphVariationProcessor? glyphVariationProcessor; private int vsIndex; /// /// Initializes a new instance of the struct. /// /// The raw charstring byte data for the glyph. /// The global subroutine buffers. /// The local subroutine buffers. /// The nominal width used as a bias for charstring width values. /// The CFF version (1 or 2). /// The optional item variation store for CFF2 blend operations. /// The optional font variations table. /// The optional axis variations table. /// The variation store index for blend operations. public CffEvaluationEngine( ReadOnlySpan charStrings, ReadOnlySpan globalSubrBuffers, ReadOnlySpan localSubrBuffers, int nominalWidthX, int version, ItemVariationStore? itemVariationStore = null, FVarTable? fVar = null, AVarTable? aVar = null, int vsIndex = 0) { this.transforming = default; this.charStrings = charStrings; this.globalSubrBuffers = globalSubrBuffers; this.localSubrBuffers = localSubrBuffers; this.nominalWidthX = nominalWidthX; this.globalBias = CalculateBias(this.globalSubrBuffers.Length); this.localBias = CalculateBias(this.localSubrBuffers.Length); this.trans = []; this.x = 0; this.y = 0; this.width = null; this.nStems = 0; this.stack = new(50); this.isDisposed = false; this.version = version; this.glyphVariationProcessor = null; if (itemVariationStore != null) { if (fVar is null) { throw new InvalidFontFileException("missing fVar table required for glyph variations processing"); } this.glyphVariationProcessor = new GlyphVariationProcessor(itemVariationStore, fVar, aVar); } this.vsIndex = vsIndex; } /// /// Computes the bounding box of the glyph by evaluating the charstring program. /// /// The of the glyph. public Bounds GetBounds() { this.Reset(); // TODO: It would be nice to avoid the allocation here. CffBoundsFinder finder = new(); // Note: scale is passed with negative Y to flip the Y axis. this.transforming = new(finder, Vector2.Zero, new Vector2(1, -1), Vector2.Zero, Matrix3x2.Identity); // Boolean IGlyphRenderer.BeginGlyph(..) is handled by the caller. this.Parse(this.charStrings); // Some CFF end without closing the latest contour. if (this.transforming.IsOpen) { this.transforming.EndFigure(); } return finder.GetBounds(); } /// /// Evaluates the charstring program and renders the glyph outline to the specified renderer. /// /// The glyph renderer to output path operations to. /// The origin point for rendering. /// The scale factor to apply. /// The offset to apply. /// The transformation matrix to apply. public void RenderTo(IGlyphRenderer renderer, Vector2 origin, Vector2 scale, Vector2 offset, Matrix3x2 transform) { this.Reset(); this.transforming = new(renderer, origin, scale, offset, transform); // Boolean IGlyphRenderer.BeginGlyph(..) is handled by the caller. this.Parse(this.charStrings); // Some CFF end without closing the latest contour. if (this.transforming.IsOpen) { this.transforming.EndFigure(); } } /// /// Parses and interprets a Type 2 charstring byte buffer, executing operators and accumulating operands. /// /// The charstring byte data to parse. private void Parse(ReadOnlySpan buffer) { SimpleBinaryReader reader = new(buffer); bool endCharEncountered = false; while (!endCharEncountered && reader.CanRead()) { byte b0 = reader.ReadByte(); if (b0 < 32) { int index; ReadOnlySpan subr; bool phase; float c1x; float c1y; float c2x; float c2y; var oneByteOperator = (Type2Operator1)b0; switch (oneByteOperator) { case Type2Operator1.Hstem: case Type2Operator1.Vstem: case Type2Operator1.Hstemhm: case Type2Operator1.Vstemhm: this.ParseStems(); break; case Type2Operator1.Vmoveto: if (this.stack.Length > 1) { this.CheckWidth(); } this.y += this.stack.Shift(); this.transforming.MoveTo(new Vector2(this.x, this.y)); this.stack.Clear(); break; case Type2Operator1.Rlineto: while (this.stack.Length >= 2) { this.x += this.stack.Shift(); this.y += this.stack.Shift(); this.transforming.LineTo(new Vector2(this.x, this.y)); } this.stack.Clear(); break; case Type2Operator1.Hlineto: case Type2Operator1.Vlineto: phase = oneByteOperator == Type2Operator1.Hlineto; while (this.stack.Length >= 1) { if (phase) { this.x += this.stack.Shift(); } else { this.y += this.stack.Shift(); } this.transforming.LineTo(new Vector2(this.x, this.y)); phase = !phase; } this.stack.Clear(); break; case Type2Operator1.Rrcurveto: while (this.stack.Length > 0) { this.transforming.CubicBezierTo( new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift()), new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift()), new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift())); } this.stack.Clear(); break; case Type2Operator1.Callsubr: index = (int)this.stack.Pop() + this.localBias; subr = this.localSubrBuffers[index]; if (subr.Length > 0) { this.Parse(subr); } break; case Type2Operator1.Return: if (this.version >= 2) { break; } return; case Type2Operator1.Endchar: if (this.version >= 2) { break; } if (this.stack.Length > 0) { this.CheckWidth(); } if (this.transforming.IsOpen) { this.transforming.EndFigure(); } endCharEncountered = true; break; case Type2Operator1.VsIndex: if (this.version < 2) { throw new NotSupportedException("blend operator is not supported in CFF v1"); } this.vsIndex = (int)this.stack.Pop(); break; case Type2Operator1.Blend: if (this.version < 2) { throw new NotSupportedException("blend operator is not supported in CFF v1"); } if (this.glyphVariationProcessor is null) { throw new NotSupportedException("blend operator in non-variation font"); } float[] blendVector = this.glyphVariationProcessor.BlendVector(this.vsIndex); float numBlends = this.stack.Pop(); float numOperands = numBlends * blendVector.Length; int delta = this.stack.Length - (int)numOperands; int basis = delta - (int)numBlends; for (int i = 0; i < numBlends; i++) { float sum = this.stack[basis + i]; for (int j = 0; j < blendVector.Length; j++) { sum += blendVector[j] * this.stack[delta++]; } this.stack[basis + i] = sum; } while (numOperands-- > 0) { this.stack.Pop(); } break; case Type2Operator1.Hintmask: case Type2Operator1.Cntrmask: this.ParseStems(); reader.Position += (this.nStems + 7) >> 3; break; case Type2Operator1.Rmoveto: if (this.stack.Length > 2) { this.CheckWidth(); } this.x += this.stack.Shift(); this.y += this.stack.Shift(); this.transforming.MoveTo(new Vector2(this.x, this.y)); this.stack.Clear(); break; case Type2Operator1.Hmoveto: if (this.stack.Length > 1) { this.CheckWidth(); } this.x += this.stack.Shift(); this.transforming.MoveTo(new Vector2(this.x, this.y)); this.stack.Clear(); break; case Type2Operator1.Rcurveline: while (this.stack.Length >= 8) { this.transforming.CubicBezierTo( new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift()), new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift()), new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift())); } this.transforming.LineTo(new Vector2(this.x += this.stack.Shift(), this.y += this.stack.Shift())); this.stack.Clear(); break; case Type2Operator1.Rlinecurve: while (this.stack.Length >= 8) { this.x += this.stack.Shift(); this.y += this.stack.Shift(); this.transforming.LineTo(new Vector2(this.x, this.y)); } c1x = this.x + this.stack.Shift(); c1y = this.y + this.stack.Shift(); c2x = c1x + this.stack.Shift(); c2y = c1y + this.stack.Shift(); this.x = c2x + this.stack.Shift(); this.y = c2y + this.stack.Shift(); this.transforming.CubicBezierTo( new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(this.x, this.y)); this.stack.Clear(); break; case Type2Operator1.Vvcurveto: if (this.stack.Length % 2 != 0) { this.x += this.stack.Shift(); } while (this.stack.Length >= 4) { c1x = this.x; c1y = this.y + this.stack.Shift(); c2x = c1x + this.stack.Shift(); c2y = c1y + this.stack.Shift(); this.x = c2x; this.y = c2y + this.stack.Shift(); this.transforming.CubicBezierTo( new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(this.x, this.y)); } this.stack.Clear(); break; case Type2Operator1.Hhcurveto: if (this.stack.Length % 2 != 0) { this.y += this.stack.Shift(); } while (this.stack.Length >= 4) { c1x = this.x + this.stack.Shift(); c1y = this.y; c2x = c1x + this.stack.Shift(); c2y = c1y + this.stack.Shift(); this.x = c2x + this.stack.Shift(); this.y = c2y; this.transforming.CubicBezierTo( new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(this.x, this.y)); } this.stack.Clear(); break; case Type2Operator1.Shortint: this.stack.Push(reader.ReadInt16BE()); break; case Type2Operator1.Callgsubr: index = (int)this.stack.Pop() + this.globalBias; subr = this.globalSubrBuffers[index]; if (subr.Length > 0) { this.Parse(subr); } break; case Type2Operator1.Vhcurveto: case Type2Operator1.Hvcurveto: phase = oneByteOperator == Type2Operator1.Hvcurveto; while (this.stack.Length >= 4) { if (phase) { c1x = this.x + this.stack.Shift(); c1y = this.y; c2x = c1x + this.stack.Shift(); c2y = c1y + this.stack.Shift(); this.y = c2y + this.stack.Shift(); this.x = c2x + (this.stack.Length == 1 ? this.stack.Shift() : 0); } else { c1x = this.x; c1y = this.y + this.stack.Shift(); c2x = c1x + this.stack.Shift(); c2y = c1y + this.stack.Shift(); this.x = c2x + this.stack.Shift(); this.y = c2y + (this.stack.Length == 1 ? this.stack.Shift() : 0); } this.transforming.CubicBezierTo(new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(this.x, this.y)); phase = !phase; } this.stack.Clear(); break; case Type2Operator1.Escape: bool a; bool b; byte twoByteOperator = reader.ReadByte(); if (twoByteOperator >= 38) { ThrowInvalidOperator(twoByteOperator); return; } switch ((Type2Operator2)twoByteOperator) { case Type2Operator2.And: a = this.stack.Pop() != 0; b = this.stack.Pop() != 0; this.stack.Push((a && b) ? 1 : 0); break; case Type2Operator2.Or: a = this.stack.Pop() != 0; b = this.stack.Pop() != 0; this.stack.Push((a || b) ? 1 : 0); break; case Type2Operator2.Not: a = this.stack.Pop() != 0; this.stack.Push(a ? 1 : 0); break; case Type2Operator2.Abs: this.stack.Push(Math.Abs(this.stack.Pop())); break; case Type2Operator2.Add: this.stack.Push(this.stack.Pop() + this.stack.Pop()); break; case Type2Operator2.Sub: this.stack.Push(this.stack.Pop() - this.stack.Pop()); break; case Type2Operator2.Div: this.stack.Push(this.stack.Pop() / this.stack.Pop()); break; case Type2Operator2.Neg: this.stack.Push(-this.stack.Pop()); break; case Type2Operator2.Eq: this.stack.Push(this.stack.Pop() == this.stack.Pop() ? 1 : 0); break; case Type2Operator2.Drop: this.stack.Pop(); break; case Type2Operator2.Put: float val = this.stack.Pop(); int idx = (int)this.stack.Pop(); this.trans[idx] = val; break; case Type2Operator2.Get: idx = (int)this.stack.Pop(); this.trans.TryGetValue(idx, out float v); this.stack.Push(v); this.trans.Remove(idx); break; case Type2Operator2.Ifelse: float s1 = this.stack.Pop(); float s2 = this.stack.Pop(); float v1 = this.stack.Pop(); float v2 = this.stack.Pop(); this.stack.Push(v1 <= v2 ? s1 : s2); break; case Type2Operator2.Random: this.stack.Push((float)Random.NextDouble()); break; case Type2Operator2.Mul: this.stack.Push(this.stack.Pop() * this.stack.Pop()); break; case Type2Operator2.Sqrt: this.stack.Push(MathF.Sqrt(this.stack.Pop())); break; case Type2Operator2.Dup: float m = this.stack.Pop(); this.stack.Push(m); this.stack.Push(m); break; case Type2Operator2.Exch: float ex = this.stack.Pop(); float ch = this.stack.Pop(); this.stack.Push(ch); this.stack.Push(ex); break; case Type2Operator2.Index: idx = (int)this.stack.Pop(); if (idx < 0) { idx = 0; } else if (idx > this.stack.Length - 1) { idx = this.stack.Length - 1; } this.stack.Push(this.stack[idx]); break; case Type2Operator2.Roll: int n = (int)this.stack.Pop(); float j = this.stack.Pop(); if (j >= 0) { while (j > 0) { float t = this.stack[n - 1]; for (int i = n - 2; i >= 0; i--) { this.stack[i + 1] = this.stack[i]; } this.stack[0] = t; j--; } } else { while (j < 0) { float t = this.stack[0]; for (int i = 0; i <= n; i++) { this.stack[i] = this.stack[i + 1]; } this.stack[n - 1] = t; j++; } } break; case Type2Operator2.Hflex: c1x = this.x + this.stack.Shift(); c1y = this.y; c2x = c1x + this.stack.Shift(); c2y = c1y + this.stack.Shift(); float c3x = c2x + this.stack.Shift(); float c3y = c2y; float c4x = c3x + this.stack.Shift(); float c4y = c3y; float c5x = c4x + this.stack.Shift(); float c5y = c4y; float c6x = c5x + this.stack.Shift(); float c6y = c5y; this.x = c6x; this.y = c6y; this.transforming.CubicBezierTo(new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(c3x, c3y)); this.transforming.CubicBezierTo(new Vector2(c4x, c4y), new Vector2(c5x, c5y), new Vector2(c6x, c6y)); this.stack.Clear(); break; case Type2Operator2.Flex: this.transforming.CubicBezierTo(new Vector2(this.stack.Shift(), this.stack.Shift()), new Vector2(this.stack.Shift(), this.stack.Shift()), new Vector2(this.stack.Shift(), this.stack.Shift())); this.transforming.CubicBezierTo(new Vector2(this.stack.Shift(), this.stack.Shift()), new Vector2(this.stack.Shift(), this.stack.Shift()), new Vector2(this.stack.Shift(), this.stack.Shift())); this.stack.Shift(); this.stack.Clear(); break; case Type2Operator2.Hflex1: c1x = this.x + this.stack.Shift(); c1y = this.y + this.stack.Shift(); c2x = c1x + this.stack.Shift(); c2y = c1y + this.stack.Shift(); c3x = c2x + this.stack.Shift(); c3y = c2y; c4x = c3x + this.stack.Shift(); c4y = c3y; c5x = c4x + this.stack.Shift(); c5y = c4y + this.stack.Shift(); c6x = c5x + this.stack.Shift(); c6y = c5y; this.x = c6x; this.y = c6y; this.transforming.CubicBezierTo(new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(c3x, c3y)); this.transforming.CubicBezierTo(new Vector2(c4x, c4y), new Vector2(c5x, c5y), new Vector2(c6x, c6y)); this.stack.Clear(); break; case Type2Operator2.Flex1: float startX = this.x; float startY = this.y; c1x = this.x + this.stack.Shift(); c1y = this.y + this.stack.Shift(); c2x = c1x + this.stack.Shift(); c2y = c1y + this.stack.Shift(); c3x = c2x + this.stack.Shift(); c3y = c2y + this.stack.Shift(); c4x = c3x + this.stack.Shift(); c4y = c3y + this.stack.Shift(); c5x = c4x + this.stack.Shift(); c5y = c4y + this.stack.Shift(); if (MathF.Abs(this.x - startX) > Math.Abs(this.y - startY)) { // horizontal c6x = c5x + this.stack.Shift(); c6y = startY; } else { c6x = startX; c6y = c5y + this.stack.Shift(); } this.x = c6x; this.y = c6y; this.transforming.CubicBezierTo(new Vector2(c1x, c1y), new Vector2(c2x, c2y), new Vector2(c3x, c3y)); this.transforming.CubicBezierTo(new Vector2(c4x, c4y), new Vector2(c5x, c5y), new Vector2(c6x, c6y)); this.stack.Clear(); break; } break; } } else if (b0 < 247) { this.stack.Push(b0 - 139); } else if (b0 < 251) { byte b1 = reader.ReadByte(); this.stack.Push(((b0 - 247) * 256) + b1 + 108); } else if (b0 < 255) { byte b1 = reader.ReadByte(); this.stack.Push((-(b0 - 251) * 256) - b1 - 108); } else { this.stack.Push(reader.ReadFloatFixed1616()); } } } /// /// Releases the resources used by the evaluation engine stack. /// public void Dispose() { if (this.isDisposed) { return; } this.stack.Dispose(); this.isDisposed = true; } /// /// Calculates the subroutine bias based on the number of subroutines, as specified in the Type 2 charstring format. /// /// The number of subroutines in the INDEX. /// The bias value to add to subroutine indices. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int CalculateBias(int count) { if (count == 0) { return 0; } return (count < 1240) ? 107 : (count < 33900) ? 1131 : 32768; } /// /// Parses stem hint operators, consuming width if present and counting hint pairs. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ParseStems() { if (this.stack.Length % 2 != 0) { this.CheckWidth(); } this.nStems += this.stack.Length >> 1; this.stack.Clear(); } /// /// Checks whether a glyph width value is present at the bottom of the stack and consumes it. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckWidth() => this.width ??= this.stack.Shift() + this.nominalWidthX; /// /// Resets the evaluation engine state for a new rendering pass. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Reset() { this.x = 0; this.y = 0; this.width = null; this.nStems = 0; this.stack.Clear(); this.trans.Clear(); } /// /// Throws an for an unrecognized charstring operator. /// /// The unrecognized operator byte value. [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowInvalidOperator(byte @operator) => throw new InvalidFontFileException($"Unknown operator:{@operator}"); } }