65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2014年7月15日

CPOL

5分钟阅读

viewsIcon

23519

downloadIcon

268

使用Cairo从C#绘制文本,并能完全控制字符定位、换行等。

引言

我对Xlib/X11的文本绘制能力越来越不满意,我在我的 Roma Widget Set (C# X11) 项目中大量使用它。特别是,字符串繁琐的国际化和抗锯齿功能的缺乏,已经成为该项目日益增长的限制。

幸运的是,CairoPango 库提供了专业的文本输出,它们基于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
© . All rights reserved.