// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using SixLabors.Fonts.Rendering;
namespace SixLabors.Fonts.Tables.Cff {
///
/// Calculates the bounding box of a CFF glyph by implementing
/// and tracking the minimum and maximum coordinates of all path operations.
///
internal class CffBoundsFinder : IGlyphRenderer
{
private float minX;
private float maxX;
private float minY;
private float maxY;
private Vector2 currentXY;
private readonly int nsteps;
private bool open;
private bool firstEval;
///
/// Initializes a new instance of the class.
///
public CffBoundsFinder()
{
this.minX = float.MaxValue;
this.maxX = float.MinValue;
this.minY = float.MaxValue;
this.maxY = float.MinValue;
this.nsteps = 3;
this.currentXY = Vector2.Zero;
this.open = false;
this.firstEval = true;
}
///
public void BeginFigure()
{
// Do nothing.
}
///
public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters)
=> true; // Do nothing.
///
public void BeginText(in FontRectangle bounds)
{
// Do nothing.
}
///
public void EndFigure()
{
this.open = false;
this.currentXY = Vector2.Zero;
}
///
public void EndGlyph()
{
if (this.open)
{
this.EndFigure();
}
}
///
public void EndText()
{
if (this.open)
{
this.EndFigure();
}
}
///
public void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds)
{
// Do nothing.
}
///
public void EndLayer()
{
// Do nothing.
}
///
public void LineTo(Vector2 point)
{
this.currentXY = point;
this.UpdateMinMax(point.X, point.Y);
this.open = true;
}
///
public void MoveTo(Vector2 point)
{
if (this.open)
{
this.EndFigure();
}
this.currentXY = point;
this.UpdateMinMax(point.X, point.Y);
}
///
public void ArcTo(float radiusX, float radiusY, float xAxisRotation, bool largeArc, bool sweep, Vector2 point)
{
// TODO: check this. I feel like we should have to implement it.
this.currentXY = point;
this.UpdateMinMax(point.X, point.Y);
this.open = true;
}
///
public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point)
{
float eachstep = 1F / this.nsteps;
float t = eachstep; // Start
for (int n = 1; n < this.nsteps; ++n)
{
float c = 1F - t;
Vector2 xy = (this.currentXY * c * c * c) + (secondControlPoint * 3 * t * c * c) + (thirdControlPoint * 3 * t * t * c) + (point * t * t * t);
this.UpdateMinMax(xy.X, xy.Y);
t += eachstep;
}
this.currentXY = point;
this.UpdateMinMax(point.X, point.Y);
this.open = true;
}
///
public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point)
{
float eachstep = 1F / this.nsteps;
float t = eachstep; // Start
for (int n = 1; n < this.nsteps; ++n)
{
float c = 1F - t;
Vector2 xy = (this.currentXY * c * c) + (secondControlPoint * 2 * t * c) + (point * t * t);
this.UpdateMinMax(xy.X, xy.Y);
t += eachstep;
}
this.currentXY = point;
this.UpdateMinMax(point.X, point.Y);
this.open = true;
}
///
public TextDecorations EnabledDecorations()
=> TextDecorations.None;
///
public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness)
{
// Do nothing.
}
///
/// Updates the tracked minimum and maximum coordinates with the given point.
///
/// The x-coordinate to evaluate.
/// The y-coordinate to evaluate.
private void UpdateMinMax(float x0, float y0)
{
if (this.firstEval)
{
// 4 times
if (x0 < this.minX)
{
this.minX = x0;
}
if (x0 > this.maxX)
{
this.maxX = x0;
}
if (y0 < this.minY)
{
this.minY = y0;
}
if (y0 > this.maxY)
{
this.maxY = y0;
}
this.firstEval = false;
}
else
{
// 2 times
if (x0 < this.minX)
{
this.minX = x0;
}
else if (x0 > this.maxX)
{
this.maxX = x0;
}
if (y0 < this.minY)
{
this.minY = y0;
}
else if (y0 > this.maxY)
{
this.maxY = y0;
}
}
}
///
/// Gets the computed bounding box from all tracked path coordinates.
///
/// The representing the glyph bounding box.
public Bounds GetBounds()
=> new(
(short)Math.Floor(this.minX),
(short)Math.Floor(this.minY),
(short)Math.Ceiling(this.maxX),
(short)Math.Ceiling(this.maxY));
}
}