如何从 True Type 字体文件中绣制字形
使用 System.Windows.Media 库从 True Type 字体文件创建字形刺绣设计
目录
引言
本文描述了获取刺绣字形的几个步骤。您可以选择任何 True Type 字体中的任何字形并对其进行刺绣。首先,我们需要从 True Type 字体文件中提取字形几何图形。第二步是在字形线上生成锯齿。生成的字形锯齿看起来像下面这样
将字形锯齿转换为刺绣格式(例如 DST)后,即可通过刺绣机在衣服上进行刺绣。
背景
本文基于无需任何商业软件即可刺绣任何 True Type 字体中任何字形的想法。
要使用的库函数
首先,应在项目中引用 .NET 库 System.Windows.Media
,并应在文件中的 namespace
声明中添加以下行
using System.Windows.Media;
True Type 字体系列文件通常包含单个字体系列。字体系列有几种字体。
根据 MSDN,字体表示 FontFamily、FontWeight、FontStyle 和 FontStretch 的组合。
Arial True Type 字体(通常安装在 Windows 平台上)有九种字体
要从字体文件中获取字体系列,我们使用 System.Windows.Media
命名空间中 Fonts
类的 GetFontFamilies
方法 如下所示:
ICollection<FontFamily> families = Fonts.GetFontFamilies(fontPath);
如果我们需要遍历字体系列的字体,我们使用来自同一 namespace
的 FontFamily
类的 GetTypefaces
类
ICollection<Typeface> typefaces = fontFamily.GetTypefaces();
Typeface
类的实例允许创建 GlyphTypeface
实例
GlyphTypeface glyph;
GlyphTypeface glyphTypeface = typeface.TryGetGlyphTypeface(out glyph) ? glyph : null;
当我们获得 GlyphTypeface
类的实例时,我们可以从其属性 CharacterToGlyphMap
获取 typeface
的字符映射。此属性的类型为 IDictionary<int, ushort>
,其中 Unicode 作为键,字形索引作为值。
要使用 glyph
几何图形,使用 GlyphTypeface
类。Glyph
几何图形通过其索引获得
Geometry geom = glyphTypeface.GetGlyphOutline(glyphIndex, renderingEmSize, hintingEmSize);
尽管更常见的是,我们希望通过其 Unicode 编号获取 glyph
。因此,我们通过其 Unicode 编号从字符映射获取 glyph
索引,然后获取 glyph
几何图形。在方法 GetGlyphOutline
中,传递了三个参数。第三个参数 hintingEmSize
在 此处 详细讨论。
在方法 GetGlyphPoints()
中,PointCollection
的枚举在作为 glyphGeom
参数传入的 PathGeometry
实例上构建。字形的 PathGeometry
实例通过 Geometry
实例的 GetOutlinedPathGeometry()
方法获得。枚举中的每个点集合表示 PathGeometry
中每个图形的点序列。这将在下一步中进行锯齿化。
/// <summary>
/// Iterate through <see cref="PathFigureCollection"/>
/// to collect enumeration of <see cref="PointCollection"/>/// </summary>
/// <param name="glyphGeom"><see cref="PathGeometry"/> of a glyph</param>
/// <returns>Returns enumeration of <see cref="PointCollection"/>
/// of flattened <see cref="PathGeometry"/></returns>
public IEnumerable<PointCollection> GetGlyphPoints(PathGeometry glyphGeom)
{
var result = new List<PointCollection>();
PointCollection curColl = new PointCollection();
foreach (PathFigure figure in glyphGeom.Figures)
{
PointCollection flattenedShape = PathFigureFlattening.GetPathFigureFlattenedPoints(figure);
result.Add(flattenedShape);
}
return result;
}
现在我们有了指定字形的点序列,它可以转换为 SVG
图片或只是 JSON
对象。尽管如此,让我们对字形的这个轮廓应用锯齿化效果。为此,我们需要将每个截距分成小截距,以便通过它们进行锯齿化。
获取截距上的中间点
在 GetPointsOnLine()
方法的以下代码中,轴方向由截距的最长轴增量选择。这意味着,如果增量 X
大于增量 Y,则选择通过 X
轴并通过固定增量值 X
计算每一步的增量 Y
。如果截距的增量 Y
大于增量 X
,则它以固定间距通过 Y
轴。这允许避免偏差。length
变量通过 勾股定理公式计算:
The square of hypothenuse is equals to sum of cathetus squares.
通过 length
和 step
值,计算截距上的点数量。对于少于六个点的短截距,它将点数量设置为七个。这允许构建锯齿并留下足够的点来创建与相邻截距的弯曲锯齿。它还会检查水平线和垂直线,在这种情况下有一段代码来计算点坐标。该方法将点作为三个坐标列表返回:前两个点、中间点和后两个点。起点和终点不包含在结果中。中间点将用于通过找到小截距中间点的垂直点进行简单的锯齿化。前两个和后两个将用于通过卡斯特里奥算法在曲线上找到与相邻截距相连的点,然后解析的点序列将用于构建锯齿化曲线。
/// <summary>
/// Returns points positioned with specified pitch between two points
/// </summary>
/// <param name="start">Start point of intercept</param>
/// <param name="end">End point of intercept</param>
/// <param name="step"></param>
/// <returns></returns>
private List<List<DoubleVertice>> GetPointsOnLine(DoubleVertice start, DoubleVertice end, double step)
{
var result = new List<List<DoubleVertice>>();
var firstTwo = new List<DoubleVertice>();
var lastTwo = new List<DoubleVertice>();
var mediPoints = new List<DoubleVertice>();
double cathetusX;
double cathetusY;
double dx;
double dy;
GetDxDy(start, end, out cathetusX, out cathetusY);
double y0 = start.Y;
double y1 = end.Y;
double x0 = (double)start.X;
double x1 = (double)end.X;
double k;
bool shortLine = false;
double deltaX = 0.0;
double deltaY = 0.0;
int length = (int)Math.Sqrt(cathetusX * cathetusX + cathetusY * cathetusY);
int n = (int) Math.Sqrt((length*length)/(step*step));
if (ShortLineIterceptsCount - 1 > n)
{
n = ShortLineIterceptsCount;
shortLine = true;
dx = dx/ShortLineIterceptsCount;
dy = dy/ShortLineIterceptsCount;
}
else
{
var deltaVector = Get_qrtnDeltaVector(x0, y0, x1, y1, step);
dx = Math.Abs(deltaVector.X);
dy = Math.Abs(deltaVector.Y);
}
if (dy == 0 && dx == 0)
{
result.Add(new List<DoubleVertice> {start, end});
return result;
}
if (dy == 0 || dx == 0)
{
if (dy == 0) // horizontal
{
deltaY = 0;
if (shortLine) step = dx;
if (x1 > x0) deltaX = step;
else deltaX = -step;
}
else if (dx == 0) //vertical
{
deltaX = 0;
if (shortLine) step = dy;
if (y1 > y0) deltaY = step;
else deltaY = -step;
}
}
else
{
if ((x0 < x1) && (y0 > y1))
{
// right up
deltaX = dx;
deltaY = -dy;
}
else if ((x0 > x1) && (y0 > y1))
{
// left up
deltaX = -dx;
deltaY = -dy;
}
else if ((x0 > x1) && (y0 < y1))
{
// left down
deltaX = -dx;
deltaY = dy;
}
else
{
deltaX = dx;
deltaY = dy;
}
}
double dX = (deltaX);
double dY = (deltaY);
deltaX = 0;
deltaY = 0;
double curX = x0, curY = y0;
if ((dx >= dy) && (dx != 0)) // goes by X Axis
{
k = dY/dX;
for (var i = 0; (i < 2) && (dX != 0); i++)
{
deltaX += dX;
deltaY = (deltaX*k);
curX = x0 + deltaX;
curY = y0 + deltaY;
firstTwo.Add(new DoubleVertice {X = curX, Y = curY});
}
for (var i = 4; (i < n) && (dX != 0); i++)
{
curX = x0 + deltaX;
curY = y0 + deltaY;
mediPoints.Add(new DoubleVertice {X = curX, Y = curY});
deltaX += dX;
deltaY = (deltaX*k);
}
for (var i = 0; (i < 2) && (dX != 0); i++)
{
curX = x0 + deltaX;
curY = y0 + deltaY;
lastTwo.Add(new DoubleVertice {X = curX, Y = curY});
deltaX += dX;
deltaY = (deltaX*k);
}
}
else if (dy != 0)
{
k = dX/dY;
for (int i = 0; (i < 2) && (dY != 0); i++)
{
deltaX = (deltaY*k);
deltaY += dY;
curY = y0 + deltaY;
curX = x0 + deltaX;
firstTwo.Add(new DoubleVertice {X = curX, Y = curY});
}
for (int i = 4; (i < n) && (dY != 0); i++)
{
curY = y0 + deltaY;
curX = x0 + deltaX;
mediPoints.Add(new DoubleVertice {X = curX, Y = curY});
deltaX = (deltaY*k);
deltaY += dY;
}
for (int i = 0; (i < 2) && (dY != 0); i++)
{
curY = y0 + deltaY;
curX = x0 + deltaX;
lastTwo.Add(new DoubleVertice {X = curX, Y = curY});
deltaX = (deltaY*k);
deltaY += dY;
}
}
result.Add(firstTwo);
result.Add(mediPoints);
result.Add(lastTwo);
if (mediPoints.Count == 0)
{
Trace.WriteLine(string.Format("{0} {1}", start, end));
}
return result;
}
计算垂直点坐标以获取锯齿
在下图中,我们有
- 截距 A B 的相邻步进点
- 截距 M 的中点
- 要找到的锯齿点 C D
为了找到锯齿点,使用了 stackoverflow 的答案。该解决方案基于 等边三角形高度计算的公式。因此,在方法 GetMediPerpendicularPoint()
中计算了 图 2 中点 C 和 D 的坐标。绿色虚线是我们将要绘制的锯齿。假设 C 是锯齿的左侧点。对于点 C,分别使用点 A 和 M 作为 point0
和 point1
调用方法。在这种情况下,参数 opposite
为 false
。因此点 D 是锯齿的右侧点,并且 opposite
为 true。我们传递点 M 和 D 作为第二个锯齿点 (D) 的 point0
和 point1
。为了获取整个截距的所有锯齿,我们遍历通过 GetPointsOnLine()
方法获得的所有中点。
等边三角形的法线是四分之三的平方根,因此以下方法中的逻辑基于此声明。
DoubleVertice GetMediPerpendicularPoint
(DoubleVertice point0, DoubleVertice point1, double distance, bool opposite)
{
double x, y, lx, ly, mx, my;
x = point1.X;
y = point1.Y;
lx = point0.X;
ly = point0.Y;
LineDirection direction;
double xPerpOffset = 0;
double yPerpOffset = 0;
var dx = x - lx;
var dy = y - ly;
mx = (lx + x) / 2;
my = (ly + y) / 2;
if (dx != 0.0 && dy != 0.0)
{
var scale = Math.Sqrt(0.75);
var dX = scale * (y - ly);
var dY = -scale * (x - lx);
var length = Math.Sqrt(dX * dX + dY * dY);
var xnorm = Math.Abs(dX / length);
var ynorm = Math.Abs(dY / length);
xPerpOffset = Math.Abs(xnorm * distance);
yPerpOffset = Math.Abs(ynorm * distance);
if ((x > lx) && (y > ly)) direction = LineDirection.UpRight;
else if ((x < lx) && (y < ly)) direction = LineDirection.DownLeft;
else if ((x > lx) && (y < ly)) direction = LineDirection.DownRight;
else direction = LineDirection.UpLeft;
switch (direction)
{
case LineDirection.UpRight:
if (opposite) yPerpOffset *= -1;
else xPerpOffset *= -1;
break;
case LineDirection.DownRight:
if (opposite)
{
yPerpOffset *= -1;
xPerpOffset *= -1;
}
break;
case LineDirection.DownLeft:
if (opposite) xPerpOffset *= -1;
else yPerpOffset *= -1;
break;
case LineDirection.UpLeft:
if (!opposite)
{
yPerpOffset *= -1;
xPerpOffset *= -1;
}
break;
}
}
else if (dx == 0.0)
{
yPerpOffset = 0.0;
if (ly < y)
{
xPerpOffset = distance * (opposite ? -1 : 1);
}
else
{
xPerpOffset = distance * (opposite ? 1 : -1);
}
}
else if (dy == 0.0)
{
xPerpOffset = 0.0;
if (lx < x)
{
yPerpOffset = distance * (opposite ? 1 : -1);
}
else
{
yPerpOffset = distance * (opposite ? -1 : 1);
}
}
return new DoubleVertice { X = mx + xPerpOffset, Y = my + yPerpOffset };
}
在截距之间创建锯齿形弯曲转角
为了在截距之间获得良好的圆角转弯,我们在截距的倒数第二个点之间添加了更多的中点。这些点通过 卡斯特里奥算法 添加。这并不是这种情况下的最佳方法,还有其他几种方法,尽管对于简单形状,它效果很好。卡斯特里奥算法的解释采纳自一些 CodeProject 文章
#region Casteljau DRAW METHOD
private List<DoubleVertice> casteljauPoints;
private List<DoubleVertice> drawCasteljau(List<DoubleVertice> list, double step)
{
casteljauPoints = list;
var result = new List<DoubleVertice>();
for (double t = 0; t <= 1; t += step)
{
DoubleVertice tmp = getCasteljauDoubleVertice(list.Count - 1, 0, t);
result.Add(tmp);
}
casteljauPoints.Clear();
return result;
}
private DoubleVertice getCasteljauDoubleVertice(int r, int i, double t)
{
if (r == 0) return casteljauPoints[i];
DoubleVertice p1 = getCasteljauDoubleVertice(r - 1, i, t);
DoubleVertice p2 = getCasteljauDoubleVertice(r - 1, i + 1, t);
return new DoubleVertice { X = ((1 - t) * p1.X + t * p2.X), Y = ((1 - t) * p1.Y + t * p2.Y) };
}
private IEnumerable<DoubleVertice> GetZigzagifiedTurn
(IEnumerable<DoubleVertice> vertices, double zigzagWidth, double step = 0.2)
{
var list = drawCasteljau(vertices.ToList(), step);
int pointCount = list.Count;
var result = new List<DoubleVertice>();
DoubleVertice prevVertex = list[0];
for (int i = 1; i < pointCount; i++)
{
var curVertex = list[i];
result.AddRange(GetZigzaggedBetweenTwoPoint(prevVertex, curVertex, zigzagWidth));
prevVertex = curVertex;
}
return result;
}
#endregion
转换为刺绣 DST 格式
如何将点序列转换为 Tajima DST 格式在 一篇文章 中有所介绍。
锯齿化字形也可以用 控制台应用程序 转换为 SVG。
结论
现在我们有了从 True Type 字体文件中获取任何字形点序列的方法,并且可以在其线上创建锯齿。如前所述,这种方法并非最佳。对于更精确的绘制,还有其他算法,我没有找到。
下图中显示了 SVG 格式的锯齿化字形
字形锯齿化的实时示例 在此处
源代码
您可以从 此处 下载源代码。
参考文献
- 如何在直线上找到锯齿点,来自 stackoverflow 的答案
- 等边三角形高度计算公式
- 卡斯特里奥算法
- 与卡斯特里奥算法相关的 CodeProject 文章
关注点
我希望缓慢绘制。
历史
- 2016 年 1 月 12 日:初始版本