U++ 中的文本到 SVG 路径工具
在 U++ 中创建一个将文本转换为 SVG 路径的简单实用程序
引言
在开发一些图形代码时,我需要一些字体字形作为 SVG 路径,以便我可以在代码中合理地绘制它们。在考察了各种选项后,我决定使用 U++ 框架编写自己的工具,这仅用了 20 分钟。尽管这是一个非常小众的工具,但我认为这个工具和代码的描述对更广泛的公众来说都可能有用。
TextToSvgPath 函数
SVG 路径是一种简单的文本格式,它使用移动/线条/曲线图元来描述图形形状。同时,所有当前的字体系统也使用完全相同的图元来绘制字形。因此,如果我们想在字体和 SVG 路径之间进行转换,我们基本上需要从字体中提取特定字形的曲线信息,并将其转换为 SVG 路径的文本表示。
在 U++ 中,字体由 `Font` 类型表示。`Font` 提供
void Render(FontGlyphConsumer& sw, double x, double y, int ch) const;
方法,该方法将表示 unicode 码点 `ch` 的字形渲染到纯虚接口 `FontGlyphConsumer`。
struct FontGlyphConsumer {
virtual void Move(Pointf p) = 0;
virtual void Line(Pointf p) = 0;
virtual void Quadratic(Pointf p1, Pointf p2) = 0;
virtual void Cubic(Pointf p1, Pointf p2, Pointf p3) = 0;
virtual void Close() = 0;
};
现在,显而易见的任务是实现这个接口,使其生成 SVG 路径输出。
struct TextToSvg : FontGlyphConsumer {
String t; // here we accumulate the SVG path text
void Put(Pointf p); // put the point as text coordinates to SVG path text
virtual void Move(Pointf p);
virtual void Line(Pointf p);
virtual void Quadratic(Pointf p1, Pointf p2);
virtual void Cubic(Pointf p1, Pointf p2, Pointf p3);
virtual void Close();
};
void TextToSvg::Put(Pointf p)
{
t << Format("%.2f %.2f ", p.x, p.y);
}
void TextToSvg::Move(Pointf p)
{
t << 'M';
Put(p);
}
void TextToSvg::Line(Pointf p)
{
t << 'L';
Put(p);
}
void TextToSvg::Quadratic(Pointf p1, Pointf p)
{
t << 'Q';
Put(p1);
Put(p);
}
void TextToSvg::Cubic(Pointf p1, Pointf p2, Pointf p)
{
t << 'C';
Put(p1);
Put(p2);
Put(p);
}
void TextToSvg::Close()
{
t << 'Z';
}
完成这些后,我们就可以实现一个函数,该函数实际上将整个文本(给定字体)转换为 SVG 路径。
String TextToSvgPath(double x, double y, const char *text, Font fnt, bool singleline)
{
WString ws = ToUnicode(text, CHARSET_DEFAULT);
TextToSvg t;
for(const wchar *s = ~ws; *s; s++) {
fnt.Render(t, x, y, *s);
x += fnt[*s]; // move the reference point
if(!singleline)
t.t << "\n";
}
return t.t;
}
添加 UI
为了完成这个工具,我们需要添加一个 GUI 对话框。它将允许用户更改字体和文本,在只读编辑器区域显示 SVG 路径,并将 SVG 路径复制到剪贴板。此外,我们还将提供渲染路径的预览。
我们开始在 U++ 布局设计器中设计对话框布局。
请注意,我们将 `preview` 字段保留为“未类型化”,这意味着我们稍后将为其提供小部件的类型。
我们将布局文件包含到代码中(这一行可以生成)
#define LAYOUTFILE <TextToSvgPath/TextToSvgPath.lay>
#include <CtrlCore/lay.h>
现在让我们准备预览小部件类。
struct Preview : Ctrl {
String svgpath;
virtual void Paint(Draw& w);
};
显然,这并不是什么高科技,我们只需要在这里保留一个生成的 SVG 路径副本,并在需要时对其进行 `Paint`。
void Preview::Paint(Draw& w)
{
DrawPainter sw(w, GetSize());
sw.Clear(SWhite());
sw.Path(svgpath).Fill(SBlack());
}
在 U++ 中,基本渲染类 `Draw` 非常简单,提供了足够的功能来绘制彩色矩形、文本和图像,并可能进行硬件加速。同时,我们有一个更精细的软件渲染器 `Painter`,它或多或少地提供了 SVG 或 PDF 渲染所需的所有图形图元。这个渲染器可以做的一项任务是渲染 SVG 路径,这正是我们所需要的。我们构建 `DrawPainter`,它是 `Draw` 和 `Painter` 之间的一种桥梁,然后我们清除背景(`SWhite` 与纯 `White` 不同,`SWhite` 会根据最终的深色主题模式进行颜色调整,在这种情况下,它实际上是黑色)。我们渲染路径并用 `SBlack` 颜色填充它(在深色主题模式下,这又是白色)。
现在是时候创建我们的对话框类了,实际上它非常简单。
struct TextToSvgPathDlg : public WithTextToSvgPathLayout<TopWindow> {
Preview preview;
void Render();
TextToSvgPathDlg();
};
在这里,我们将创建的布局添加到 `TopWindow` 类(代表顶层窗口)中,并使用结果作为我们对话框类的基础。这将所有小部件添加为成员变量,除了那些“未类型化”的小部件,例如 `preview` 小部件,因此我们将其作为成员变量在此处添加。我们在这里定义的唯一方法是 `Render`,因为这将是对话框对大多数用户操作的响应。
然后大部分工作在构造函数中完成。
TextToSvgPathDlg::TextToSvgPathDlg()
{
CtrlLayout(*this, "Text to SVG path converter");
`CtrlLayout` 函数调用 `WithTextToSvgPathLayout` 中的一些方法来实际将所有小部件放置在对话框上。
现在我们将填充字体 `face` 选择器(`DropList` 小部件)。U++ 中的字体人脸是简单索引的。索引 0 保留给默认的 GUI 字体,因此我们从 1 开始。
for(int i = 1; i < Font::GetFaceCount(); i++)
if(Font::GetFaceInfo(i) & Font::SCALEABLE)
face.Add(i, Font::GetFaceName(i));
`DropList` 选择器的条目可以有两个值:“实际”值,当被选中时,它代表小部件的值,以及“显示”值,也就是用户实际看到的内容。因此,我们将人脸的索引作为实际值,并使用 `Font::GetFaceName` 获取的文本描述作为显示值。
我们预设 `SANSSERIF face`,这是一个预定义的字体索引,对应于 Win32 主机上的 Arial 字体。
face <<= Font::SANSSERIF;
(U++ 中的运算符 `<<=` 对小部件进行了重载,表示“赋值”。然后我们继续设置高度选择器,包括限制、预定义值列表和初始设置 128 像素。
height.MinMax(6, 500);
for(int i = 4; i < 500; i += i < 16 ? 1 : i < 32 ? 4 : i < 48 ? 8 : 16)
height.AddList(i);
height <<= 128;
现在我们使我们将要用于显示 SVG 路径的编辑器字段变为只读。
svgpath.SetReadOnly();
并为我们的 `preview` 字段添加一些边框。
preview.SetFrame(ViewFrame());
现在是核心操作的时候了。我们实际想要的是,如果用户更改了任何设置或文本,我们都会重新渲染 SVG 路径并将其显示在 `svgpath` 和 `preview` 小部件中。由于这是“任何设置”,我们将偷懒,简单地遍历对话框中的所有小部件,并为所有小部件分配渲染操作,除了 `Button`。
for(Ctrl *q = GetFirstChild(); q; q = q->GetNext())
if(!dynamic_cast<Button *>(q))
*q << [=] {
Font fnt(~face, ~height);
fnt.Bold(~bold);
fnt.Italic(~italic);
svgpath <<= preview.svgpath =
TextToSvgPath(0, 0, (String)~text, fnt, ~singleline);
preview.Refresh();
};
运算符 `<<` 向小部件添加默认操作,当用户更改小部件的值或状态时会调用该操作。在这种情况下,该操作会根据对话框中小部件的值构建一个 `Font` 实例,然后使用 `TextToSvgPath` 将文本与给定字体转换为 SVG 路径。然后将此 `String` 分配给 `svgpath` 编辑器字段和 `preview` 的 SVG 路径。最后,`Refresh` 会调用 `preview` 的重绘。
最后的润色是设置按钮以将路径复制到剪贴板。
copy.SetImage(CtrlImg::copy());
copy << [=] {
WriteClipboardText(preview.svgpath);
};
并使对话框可调整大小。
Sizeable().Zoomable();
}
最后,我们需要编写 U++ 相当于 `main` 函数的内容,这很简单。
GUI_APP_MAIN
{
TextToSvgPathDlg().Run();
}
结论
说实话,我可能已经找到了使用现有工具完成此任务的方法,或者找到了类似的实用程序。另一方面,使用 U++ 完成这项工作非常简单,以至于我可能通过重新发明轮子节省了一点时间。无论如何,开发这段代码很有趣,并且将其发布到互联网上意味着任何需要类似东西的人都可以从这里下载。
有用链接
历史
- 2020 年 3 月 20 日:初始版本
- 2020 年 9 月 29 日:添加了“开始使用...”文章的链接
- 2020 年 11 月 14 日:更新了有用链接
- 2021 年 6 月 23 日:更新了有用链接