编程 Cairo 文本输出超越 'toy' 文本 API (C# X11) - 概念验证。





5.00/5 (2投票s)
使用Cairo从C#绘制文本,并能完全控制字符定位、换行等。
引言
我对Xlib/X11的文本绘制能力越来越不满意,我在我的 Roma Widget Set (C# X11) 项目中大量使用它。特别是,字符串繁琐的国际化和抗锯齿功能的缺乏,已经成为该项目日益增长的限制。
幸运的是,Cairo 和 Pango 库提供了专业的文本输出,它们基于UTF-8编码的字符串,并提供了许多炫酷的显示特性 - 包括抗锯齿、渐变和轮廓。这两个库都已集成到大多数Linux发行版中。
GTK+ UI工具包从2005年发布的2.8版本开始使用Cairo来渲染其大部分控件,并且从2002年发布的2.0版本开始使用Pango进行文本渲染。
背景
由于 Mono.Cairo 包已经封装了Cairo C API,因此尝试使用该包进行文本绘制是很自然的。不幸的是,Mono.Cairo
只提供了“玩具”文本API。尽管 Cairo.Context.ShowText()
和 Cairo.Context.TextExtents()
提供了许多炫酷的文本显示特性 - 包括抗锯齿 - 但功能仍然不足,因为它总是同时处理要绘制的文本。
使用 Cairo.Context.ShowGlyphs()
和 Cairo.Context.GlyphsExtents()
可以克服这个限制,但是Mono.Cairo
没有将UTF-8字符串转换为字形的(glyphs)方法。而这种转换是一项艰巨的任务。
- 不仅字形索引因字体系列而异(请参阅应用程序截图,并查看“'Luxi sans' writing three '36' glyphs: AAA”和“'Utopia' writing three '33' glyphs: AAA”这几行 - 输出都是“AAA”,但字形索引不同),
- 字体系列中的 字形 是在一个 cmap 中组织的,而一个cmap可能有不同的格式(在网上搜索“character to glyph mapping”或“OpenFont glyph”以获取更多信息)。
通常,这个转换是Pango库的工作,但是目前没有可用于Mono的Pango封装包可以单独提供Pango。相反,Pango高度集成在 gtk-sharp 包中。
由于我的Roma Widget Set项目应该完全避免任何GTK+的东西(不是因为它不好,只是为了避免与Gtk#竞争),我不得不找到一种不同的方法将UTF-8字符串转换为字形。讽刺的是,gtk-sharp
包中包含的Cairo的C# 源代码为我提供了一个可行的解决方案。
Using the Code
示例应用程序使用 Mono Develop 2.4.1 和 Mono 2.8.1 在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面环境下编写。移植到任何更早或更晚的版本应该都不是问题。示例应用程序的解决方案包含一个项目,其中包含了所有必需的源代码。
该示例应用程序还通过了 Mono Develop 3.0.6 在 OPEN SUSE 12.3 Linux 64 位 DE 和 GNOME 桌面、IceWM、TWM 和 Xfce 上针对 Mono 3.0.4 的测试。
Xlib/X11窗口处理基于 X11Wrapper 程序集版本0.5,它定义了Xlib/X11调用 libX11.so 的函数原型、结构和类型。它已为 使用Mono Develop编程Xlib - 第一部分:底层(概念验证) 项目开发,并在 使用Mono Develop编程Xlib - 第一部分:底层(概念验证) 项目的 Roma Widget Set (C# X11) - 一个零依赖GUI应用程序框架 - 第一部分,基础 项目中得到改进。
示例应用程序显示了一些文本输出,使用
- Cairo的“玩具”文本API
- 一个自提供的、围绕基本Cairo“玩具”文本API函数(如
cairo_get_current_transformation_matrix()
,cairo_set_current_transformation_matrix()
,cairo_set_source_rgba()
,cairo_move_to()
,cairo_show_text()
和cairo_set_scaled_font()
)的封装。 - 以及 - 最后 - 使用Cairo的
cairo_scaled_font_text_to_glyphs()
进行自提供的字符串到字形的转换器。
示例应用程序提供完整的源代码。使用自提供的字符串到字形转换器的基本步骤如下:
// Load and remember the font. Cairo uses the last loaded font for 'toy' text API.
context.SelectFontFace ("Sans", FontSlant.Normal, FontWeight.Normal); // Georgia // Courier
Cairo.FontFace ffSans = context.ContextFontFace;
// Prepare the scaled font for glyph processing.
Cairo.Matrix fm = new Cairo.Matrix (/*font size*/ 20.0, 0.0, 0.0, /*font size*/ 20.0,
/*translationX*/ 0.0, /*translationY*/ 0.0);
Cairo.Matrix tm = new Cairo.Matrix (1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
FontOptions fo = new FontOptions();
Cairo.ScaledFont sfSans = new ScaledFont (ffSans, fm, tm, fo);
fo.Dispose ();
// Draw some text with the 'toy' text API.
Cairo.CairoWrapper.SetScaledFont (context, sfSans);
Cairo.CairoWrapper.MoveTo (context, 15, 270);
Cairo.CairoWrapper.ShowText (context, "'Sans' writing some glyphs converted with");
// Convert string into glyphs.
Cairo.Glyph[] glyphs;
Cairo.CairoWrapper.ScaledFontTextToGlyphs (sfSans,
"the self-provided converter: μ-∑-√-‡-€-™", 15, 295, out glyphs);
// Draw the text from converted glyphs.
Cairo.CairoWrapper.ShowGlyphs (context, glyphs);
// Clean up.
sfSans.Dispose();
ffSans.Dispose();
下一步可能是将额外的Cairo封装功能移到Cairo方法扩展中。
要运行可执行文件,请在32位系统上启动/bin/Debug/32/XrwCairo.exe,或在64位系统上启动/bin/Debug/64/XrwCairo.exe。
要加载项目,请在32位系统上使用XrwCairo32.sln,或在64位系统上使用XrwCairo64.sln。
主要发现
标准编码到UTF-8编码转换
gtk-sharp
包中包含的Cairo的C#源代码已包含一个private
方法TerminateUtf8()
,该方法可以很好地完成工作 - 只有一个地方令人烦恼:
一个字符的UTF-8编码可能消耗1到3个字节。TerminateUtf8()
实现始终返回一个足够长的字节数组,以存储最坏情况(所有字符都需要最大字节数才能转换)。未使用的字节设置为0
。
这就是自提供的TerminateUtf8
()派上用场的地方。
// Tested: OK
/// <summary>Convert a standard string to a byte array, that ends with '\0'.</summary>
/// <param name="s">The standard string to convert.<see cref="System.String"/></param>
/// <param name="clean">Determine whether to clean tailing unused bytes.
/// <see cref="System.Boolean"/></param>
/// <returns>The guaranteed terminated UTF-8 byte array.<see cref="System.Byte[]"/></returns>
private static byte[] TerminateUtf8 (string s, bool clean)
{
// Compute the byte count including the trailing \0.
int byteCount = System.Text.Encoding.UTF8.GetMaxByteCount(s.Length + 1);
byte[] bytes = new byte[byteCount];
// Compute the UTF-8 bytes.
System.Text.Encoding.UTF8.GetBytes(s, 0, s.Length, bytes, 0);
if (!clean)
return bytes;
// Count tailing unused bytes.
int realLength = byteCount;
for (int countByte = byteCount - 1; countByte >= 0 && bytes[countByte] == 0; countByte--)
realLength--;
// Clean tailing unused bytes.
byte[] result = new byte[realLength + 1];
if (realLength > 0)
Array.Copy (bytes, result, realLength);
result[realLength] = 0;
// Done.
return result;
}
托管字形到非托管内存转换(及返回)
gtk-sharp
包中包含的Cairo的C#源代码还包含一个internal
方法FromGlyphToUnManagedMemory()
,该方法需要提供字形给cairo_show_glyphs()
。不幸的是,这个实现依赖于Context
类的实现,无法单独调用。这是我重新设计的代码:
// Copyright: Please see "Cairo.cs"!
// Tested: OK
/// <summary>Convert a glyph array to its equivalent unmanaged memory representation.</summary>
/// <param name="glyphs">The array of glyphs to convert.<see cref="Glyph[]"/></param>
/// <returns>The unmanaged memory representation of a glyph array.<see cref="IntPtr"/></returns>
internal static IntPtr FromGlyphToUnManagedMemory(Glyph [] glyphs)
{
IntPtr dest = IntPtr.Zero;
int ptrSize = Marshal.SizeOf (typeof (IntPtr));
if (ptrSize != 4)
{
int native_glyph_size = Marshal.SizeOf (typeof (Glyph));
dest = Marshal.AllocHGlobal (native_glyph_size * glyphs.Length);
long pos = dest.ToInt64();
foreach (Glyph g in glyphs)
{
Marshal.StructureToPtr (g, (IntPtr)pos, false);
pos += native_glyph_size;
}
}
else
{
int native_glyph_size = Marshal.SizeOf (typeof (NativeGlyph_4byte_longs));
dest = Marshal.AllocHGlobal (native_glyph_size * glyphs.Length);
long pos = dest.ToInt64();
foreach (Glyph g in glyphs)
{
NativeGlyph_4byte_longs n = new NativeGlyph_4byte_longs (g);
Marshal.StructureToPtr (n, (IntPtr)pos, false);
pos += native_glyph_size;
}
}
return dest;
}
基于此,我还实现了相反的方向FromUnManagedMemoryToGlyph()
,它对于ScaledFontTextToToGlyph()
是必需的。
// Tested: OK
/// <summary>Convert an unmanaged memory representation of glyphs to an array of glyphs.
/// </summary>
/// <param name="ptr">The unmanaged memory representation of glyphs to convert.
/// <see cref="IntPtr"/></param>
/// <param name="length">The number of glyphs to convert.<see cref="System.Int32"/></param>
/// <returns>The converted glyph array.<see cref="Glyph[]"/></returns>
internal static Glyph[] FromUnManagedMemoryToGlyph (IntPtr ptr, int length)
{
Glyph[] glyphs = new Glyph[Math.Max (0, length)];
if (length <= 0)
return glyphs;
int ptrSize = Marshal.SizeOf (typeof (IntPtr));
if (ptrSize != 4)
{
int native_glyph_size = Marshal.SizeOf (typeof (Glyph));
long pos = ptr.ToInt64();
for (int glyphCount = 0; glyphCount < length; glyphCount++)
{
glyphs[glyphCount] = (Glyph) Marshal.PtrToStructure ((IntPtr)pos, typeof(Glyph));
pos += native_glyph_size;
}
}
else
{
int native_glyph_size = Marshal.SizeOf (typeof (NativeGlyph_4byte_longs));
long pos = ptr.ToInt64();
NativeGlyph_4byte_longs buffer;
for (int glyphCount = 0; glyphCount < length; glyphCount++)
{
buffer = (NativeGlyph_4byte_longs) Marshal.PtrToStructure ((IntPtr)pos,
typeof(NativeGlyph_4byte_longs));
glyphs[glyphCount] = new Glyph (buffer.index, buffer.x, buffer.y);
pos += native_glyph_size;
}
}
return glyphs;
}
这两种实现都适用于32位和64位环境,并通过void*
指针的大小来区分环境:Marshal.SizeOf (typeof (IntPtr))
。
UTF-8文本到字形转换
围绕cairo_scaled_font_text_to_glyphs()
的自提供封装如下所示:
// Tested: OK
// This method has been the final target to enable glyph drawing via Context.ShowGlyphs.
/// <summary>Convert an UTF-8 text to glyphs, using the indicated scaled font.</summary>
/// <param name="scaledFont">The scaled font, required to convert UTF-8 text to glyphs.
/// <see cref="ScaledFont"/></param>
/// <param name="utf8text">The UTF-8 text to convert.
/// <see cref="System.String"/></param>
/// <param name="startX">The X start position of the first glyph.
/// <see cref="System.Double"/></param>
/// <param name="startY">The Y start position of the first glyph.
/// <see cref="System.Double"/></param>
/// <param name="glyphs">The glyph array as result of the convertion.
/// <see cref="Glyph[]"/></param>
/// <returns>The Cairo status (success or error) of the convertion.
/// <see cref="Status"/></returns>
public static Status ScaledFontTextToGlyphs(ScaledFont scaledFont, string utf8text,
double startX, double startY, out Glyph[] glyphs)
{
byte[] terminatedUtf8 = TerminateUtf8(utf8text, true);
IntPtr arrGlyph;
int numGlyph;
Status status =
NativeMethodsEx.cairo_scaled_font_text_to_glyphs (scaledFont.Handle, startX, startY,
terminatedUtf8,
terminatedUtf8.Length - 1,
ref arrGlyph, out numGlyph,
IntPtr.Zero,
IntPtr.Zero, IntPtr.Zero);
if (status != Status.Success)
{
glyphs = new Glyph[0];
return status;
}
if (arrGlyph != IntPtr.Zero && numGlyph > 0)
glyphs = FromUnManagedMemoryToGlyph (arrGlyph, numGlyph);
else
glyphs = new Glyph[0];
//if (textExtends != null)
// NativeMethods.cairo_scaled_font_glyph_extents (scaledFont.Handle,
// arrGlyph, numGlyph, out textExtends);
NativeMethodsEx.cairo_glyph_free (arrGlyph);
return status;
}
其中cairo_scaled_font_text_to_glyphs()
以及其他本地Cairo方法定义如下:
internal static class NativeMethodsEx
{
const string cairo = "libcairo-2.dll";
// Tested: OK
// If clusters are required.
[DllImport (cairo, CallingConvention=CallingConvention.Cdecl)]
internal static extern Status
cairo_scaled_font_text_to_glyphs (IntPtr scaled_font, double x, double y,
byte[] utf8, int utf8_len,
ref IntPtr glyphs, out int num_glyphs,
ref IntPtr clusters, out int num_clusters,
ref IntPtr cluster_flags);
// Tested: OK
// If clusters are NOT required.
[DllImport (cairo, CallingConvention=CallingConvention.Cdecl)]
internal static extern Status
cairo_scaled_font_text_to_glyphs (IntPtr scaled_font, double x, double y,
byte[] utf8, int utf8_len,
ref IntPtr glyphs, out int num_glyphs,
IntPtr clusters, IntPtr num_clusters,
IntPtr cluster_flags);
// Tested: OK
[DllImport (cairo, CallingConvention=CallingConvention.Cdecl)]
internal static extern void
cairo_show_text_glyphs (IntPtr scaled_font, byte[] utf8, int utf8_len,
IntPtr glyphs, int num_glyphs,
ref IntPtr clusters, ref int num_clusters,
ref /*ClusterFlags*/ IntPtr cluster_flags);
// Tested: OK
[DllImport (cairo, CallingConvention=CallingConvention.Cdecl)]
internal static extern void cairo_glyph_free (IntPtr glyphs);
// Tested: OK
[DllImport (cairo, CallingConvention=CallingConvention.Cdecl)]
internal static extern void cairo_text_cluster_free (IntPtr glyphs);
}
关注点
Cairo.CairoWrapper
类包含了Mono.Cairo
包之外所有必需的结构和辅助方法,用于将字符串转换为字形数组,测量字形(以实现自动换行)并绘制它们。
历史
- 2014年7月14日:初始版本
- 2014年7月29日:一些拼写错误和次要bug