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

Unicode 艺术

2017 年 4 月 1 日

CPOL

16分钟阅读

viewsIcon

72339

downloadIcon

1450

与 ASCII 艺术类似,Unicode 艺术提供了更好的色调范围、令人印象深刻的外观,并且在这个阳光明媚的四月天带来了一些乐趣。

引言

我宁愿被华美文字装饰,而非珠宝。

普劳图斯

目录

引言

在这个美好的春日,是时候找点乐子了。

ASCII 艺术相比,它的 Unicode 模拟体更容易自动生成;同时,由于文字的色调更好——Unicode 中有太多文字了——结果可以看起来更令人印象深刻

人类的视觉感知是一件极其复杂的事情。它呈现了视觉表现一切的诸多微妙问题。另一方面,人类感知的力量却被严重低估了。因此,为转换为精美的 Unicode 艺术形式而准备照片或其他图像需要一些艺术性。

同时,本文介绍的应用程序可以帮助快速获得一些基本经验,因为几乎所有尺寸适中、对比度明显且清晰的照片输入后,它都能生成相当可识别的字符组成的图像。让我们看看它是如何实现的。

操作 (Operation)

应用程序的操作非常简单。用户选择一个输入图像文件和用于渲染输出图像的字体。“预览”会在一个单独的窗口中渲染图像,该图像稍后可以保存为三种形式之一:图像文件、纯文本文件或 HTML。

预览可以在两种模式之间切换:一种是显示整个图像并缩放到适应窗口大小的Viewbox,另一种是“原始像素尺寸”,在ScrollViewer中查看。对于“原始像素尺寸”,如果使用的字体大小例如是 12 像素,则每个字符都将逐像素显示,在屏幕上占用恰好 12x12 像素。

输入图像应仔细准备。如果图像尺寸与字体尺寸组合过大,渲染过程可能需要很长时间。但是,每个预览窗口都可以单独中止。为获得精美渲染效果而准备输入图像需要一些练习。字体参数很重要,但图像的色调范围及其清晰度则更为关键。从最初的尝试来看,很难预测结果会是什么样子。

这就是为什么有些转换会产生一些令人惊讶的结果。

为了保留原始的纵横比,代表像素的字符始终在方形区域内渲染,该方形的边长等于字体大小。由于这种设计,字体不一定是等宽的:在所有情况下,如果字符字形的尺寸不同,它将被放置在正方形的中心。这可能会降低可见字符密度,但输出图像仍然可以很好。

研究

研究不同字体系列、度量和字符字集如何渲染某些图像的动态范围,确实需要付出一些努力。

为了映射字符子集并选择用于表示某个像素值的字符,我们首先需要收集特定字符子集中每个字符字形的“亮度”统计信息。“亮度”(引号是故意的)可以定义为在尺寸为S x S的白色背景区域(其中S是字体大小)中以黑色渲染的字形中,位值的平均值。以此方式计算的值不能正确反映人类对“颜色强度”的感知,而颜色强度本质上是对数的,但在当前情况下是可以接受的;在所有情况下,它都定义了字符按亮度的部分排序。

目标像素格式是每像素 8 位的灰度图,这足以满足需求,考虑到字符集有限的色调范围。即使使用这种格式,0..255 的色调,也没有足够的像素来覆盖所有色调。因此,我们可以将给定的字符集映射到这 256 种色调,并进行归一化映射,使“最暗”的字符映射到 0,空白字符映射到 255。问题是:即使在这个 0..255 的范围内,也不是所有值都能与映射中的某个字符对应。显然,“浅色”字符太多,而“深色”字符严重不足。给定集合中对应于某个字符的像素值比率可以被认为是字体参数和字符集组合的一种可能的“质量”特征。

这就是映射的计算方式。

for (int index = 0; index < brightnessValues.Length; ++index) {
    char symbol = list[index];
    double symbolBrightness = brightnessValues[index];
    double dValue = (symbolBrightness - min) / (1 - min) * (byte.MaxValue);
    body[(int)dValue].Add(symbol);
} //loop

这里,min是找到的最小字符字形“亮度”,而body是字符列表的array[0..255]。在此步骤之后,缺失的输出像素值(body数组的元素)通过插值填充。有关更多详细信息,请参阅源文件“CharacterRepertoire.cs”。

令人惊讶的是,计算字符的“亮度”并不需要很大的字体大小。事实上,系统字符渲染方法已经能够很好地将小字形映射到灰度色调,因此大部分平均工作已经完成。令人惊讶的是,我的测量结果显示,对于大多数字体,近乎最优的质量(如上定义)在 6 到 8 像素的字体大小下就能实现。

这就是一个接近最优情况下的字符字形“亮度”直方图的外观。

(在此图中,输入像素值 175 处的最大值超出了量程;映射到此像素值的实际字符数为 1062。显然,“几乎所有”字符的字形密度值都非常接近,落在 0..255 色调范围内的像素值 175 处。字体就是这样设计的。字体艺术家认为能够产生均匀文本行密度是艺术的主要优点之一。这也是我选择直方图中字符代码点范围的原因:它包含许多“特殊”字符,如块元素、框绘制符号、数学符号、符号集等。这些字符的字形密度非同寻常。)

事实证明,与输入图像的渲染相比,此计算速度足够快。起初,我想预先计算字符集映射并将其存储在数据中以供应用程序每次运行时使用,但这似乎对性能影响不大。

性能

图像渲染相对较慢:尺寸适中的源图像需要几秒钟才能渲染,但我尝试过的尺寸较大的图像则需要数小时。请记住,输出文件的大小与字体大小的平方成正比。

最初,字符集被映射到对应于像素值 0..255 的色调范围。此计算仅在应用程序生命周期内执行一次,然后重复使用。严格来说,应该为每个字体单独执行此计算,因为它可能影响字符的“亮度”排序,但我没有注意到任何显著差异。在我的系统上,此部分的计算在第一次运行时会增加几秒钟。任何尺寸适中的输入文件渲染都需要更长的时间。

因此,使用多线程很重要。

多线程

首先,输出图像被渲染为System.Windows.Media.Imaging.BitmapSource。用于在预览中显示图像的运行时类型是System.Windows.Media.Imaging.RenderTargetBitmap。尽管渲染需要使用System.Windows.Media.DrawingContext,但在此特定情况下,直接渲染的预览性能极差,原因非常明显。

因此,使用了RenderTargetBitmap。首先,当用户想要保存图像文件时,需要它来生成图像文件。同时,该类型的实例永久(针对“预览”窗口)用于显示渲染的图像,作为Image.Source

每个预览窗口都使用一个单独的线程。这种情况并不典型,因为进程线程的数量是无限的,但对于此应用程序来说是合理的。重要的是,用户可以在等待之前启动的渲染线程时选择渲染另一个图像,并有可能在耗时过长时中止它们。

实现的核心是线程包装器的概念,我曾在我的文章《一次回答许多问题:使用 Forms 进行交互式动画图形》、《现代 C++ 的线程包装器》和《现代 C++ 的输送带线程包装器》中开始解释。这是针对当前应用程序的一种临时包装器。

class ThreadWrapper {

    internal ThreadWrapper(
        PreviewWindow presentation,
        Image image,
        Main.Renderer renderer, BitmapImage source,
        Typeface typeface, int fontSize,
        Dispatcher dispatcher) {
            this.presentation = presentation;
            this.image = image;
            this.typeface = typeface;
            this.fontSize = fontSize;
            this.renderer = renderer;
            this.source = source;
            source.Freeze();
            this.dispatcher = dispatcher;
    } //ThreadWrapper

    internal void Start() {
        thread = new Thread(Body);
        thread.TrySetApartmentState(ApartmentState.STA);
        thread.Start();
    } //Start

    internal void Abort() {
        thread.Abort();
    } //Abort

    internal void Join() {
        thread.Join();
    } //Join

    void Body() {
        try {
            ImageSource imageSource = renderer.Render(source, typeface, fontSize, 0);
            dispatcher.Invoke(new System.Action(() => {
                image.Source = imageSource;
                image.Width = source.Width * fontSize;
                image.Height = source.Height * fontSize;
                presentation.menu.Visibility = Visibility.Visible;
                presentation.SetTarget(RenderTarget.scaled);
            }));
        } catch (ThreadAbortException) {
        } catch (System.Exception e) {
            dispatcher.Invoke(
                new System.Action<Exception>((Exception exception) => {
                    presentation.ShowException(exception);
                }), e);
        } //exception
    } //Body

    Thread thread;
    Dispatcher dispatcher;
    Main.Renderer renderer;
    BitmapImage source;
    PreviewWindow presentation;
    Image image;
    Typeface typeface;
    int fontSize;

} //class ThreadWrapper

请注意,线程可以异步中止,这有时会遭到其他开发人员的强烈反对。这是一个太大太复杂的主题,无法在此详述,因此我将仅声明:我以特定于每个应用程序的安全方式来执行此操作。

关于多线程的一个重要点是线程同步。在当前应用程序中,有两个地方需要在这两个线程之间传递数据:UI 线程和渲染每个预览实例的线程。首先,输入图像的引用被传递给图像渲染线程。最后,渲染后的ImageSource被传递给 UI 线程进行显示,作为Image.Source属性。这两个转换都基于Freeze方法:请参阅System.Windows.Freezable.Freeze

HTML 渲染

乍一看,人们可能会认为文本和 HTML 呈现 Unicode 艺术很容易实现。然而,我第一次尝试就搞砸了。以下是遗漏的几点:

  1. 字符集中的某些字符可能不受特定字体支持。在位图渲染中,这不会造成大问题,因为所有字符都在输出位图的相同固定区域内渲染。在 HTML 中,这些字符可能仍会显示并占用一些空间,但即使是等宽字体,其宽度也可能不同,这会导致文本行的水平偏移。
  2. 意外的是,某些字符可能充当行尾字符。对于 HTML 的pre元素,这会导致换行,可能在步长的中间。
  3. 意外的是,某些字符序列可能匹配 HTML 字符实体。

前两个问题应该在构建字符集时解决。重要的是要自动过滤掉一些字符。这是CharacterRepertoire.Build的片段:

internal void Build(Typeface typeface) {
    GlyphTypeface glyphTypeface;
    bool success = typeface.TryGetGlyphTypeface(out glyphTypeface);
    var map = glyphTypeface.CharacterToGlyphMap;            
    System.Predicate<ushort> isCharacterSupported = (codePoint) => {
        ushort glyphIndexDummy;
        return map.TryGetValue(codePoint, out glyphIndexDummy);
    }; //isCharacterSupported
    CharacterList list = new CharacterList();
    DoubleList brightnessList = new DoubleList();
    foreach (var range in DefinitionSet.charset)
        for (ushort codePoint = range.first;
            codePoint <= range.last;
            ++codePoint)
        {
            if (!isCharacterSupported(codePoint)) continue;
            char character = System.Char.ConvertFromUtf32(codePoint)[0];
            if (char.IsSeparator(character) ||
                char.IsControl(character))
                    continue;
            list.Add(character);
            brightnessList.Add(GetBrightness(character, typeface));
        } //loop
    // ...
}; //Build

此外,还有一些非常明显的问题,首先是纵横比。显然,如果所选字体系列具有可变尺寸字符,图像将会严重变形——尽管备用系列是monospace,但如果“Unicode Art”应用程序成功使用了某个更专业的系列,那么在浏览器渲染中也会找到相同的系列。对此我们无能为力,但如何处理等宽字体系列呢?这是 HTML 渲染的另一个微妙方面。这是解决方案:

internal void SaveToHtml(
        string fileName,
        string originalFileName,
        Typeface typeface, int fontSize)
{
    if (originalFileName == null) originalFileName = string.Empty;
    int xMax = unicodePixels.GetUpperBound(1);
    int yMax = unicodePixels.GetUpperBound(0);
    StringBuilder sb = new StringBuilder();
    for (var indexY = 0; indexY <= yMax; ++indexY) {
        for (var indexX = 0; indexX <= xMax; ++indexX)
            sb.Append(unicodePixels[indexY, indexX]);
        if (indexY < yMax) sb.Append(System.Environment.NewLine);
    } //loop Y
    GlyphTypeface glyphTypeface;
    typeface.TryGetGlyphTypeface(out glyphTypeface);
    System.Collections.Generic.IDictionary<ushort, double>
        advanceHeights = glyphTypeface.AdvanceHeights;
    System.Collections.Generic.IDictionary<ushort,
        double> advanceWidths = glyphTypeface.AdvanceWidths;
    double aspect = 1;
    double width, height;
    ushort testCharachter = DefinitionSet.fullSizeSampleCharacter;
    if (advanceWidths.TryGetValue(testCharachter, out width) &&
        advanceHeights.TryGetValue(testCharachter, out height))
            // then sample character is supported by typeface
            aspect = width / height;
    string html = string.Format(
        Resources.Resources.HtmlFormat,
        originalFileName,
        1,
        aspect,
        typeface.FontFamily.ToString(),
        System.Web.HttpUtility.HtmlEncode(sb.ToString()));  
    using (StreamWriter writer =
        new StreamWriter(fileName, false, Encoding.UTF8)) {
        writer.WriteLine(html);
    } //using
} //SaveToHtml

请注意,第三个问题是通过使用HtmlEncode解决的。现在,让我们看看渲染的 HTML 文件:

<html>
<head>
    <title>Unicode Art</title>
    <meta name="generator" content="UnicodeArt.exe" />
    <meta name="description"
    content="Generated from file: input.cow.png; ..." />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style type="text/css">
        pre {
        font-size: 1em;
        line-height: 0.748933577087142em;
        font-family: "Courier New", monospace;
    }
    </style>
</head>
<body>

<pre><!-- image characters go here... --></pre>

</body>
</html>

这是渲染结果。

(从相同的原始图像渲染的 Unicode 艺术图像看起来可能不同,因为使用了不同的字符集。)

HTML 浏览器在处理此类图像时存在一些问题。从上面的代码示例中可以看到,HTML 文件格式中插入了 5 个参数(请参阅源文件“Resources/HtmlFormat.html”):原始文件名、CSS 样式属性font-size(始终为 1 em单位)、line-heightfont-family的值,然后是格式化的字符流。这些参数最终根据应用程序主窗口中选择的字体面计算得出。

但纵横比呢?答案是:“几乎正确”。使用标准的 Web 浏览器缩放功能,可以轻松观察到纵横比有些偏差:缩放会导致纵横比在一定程度上偏离,具体取决于缩放级别。

同时,HTML 呈现非常方便,因为它可以在即时渲染图像的字形,所以我们可以快速查看单独的字符或整体视图。

新的样式元素

很久以前,我提出了一些或多或少新鲜的风格元素。我们经常需要延迟关闭窗口,该窗口可能退出整个应用程序,也可能不退出,通常是让用户选择保存某些工作或取消更改。在 WPF 中,这可以通过重写System.Windows.Window.OnClosing方法或在System.Windows.Window.Closing事件的处理器中完成。

谁说必须是那些模态对话框?为什么那些对话框要占用那么多空间?那些丑陋的按钮(无论它们看起来多么现代)有什么实用功能,它们都有阴影、边框和装饰元素之间的间距?谁说所有这些视觉线索真的能帮助用户理解应用程序的要求?它不能看起来更简单、更少干扰吗?这是我的设想:

这只是消息和选择菜单,可自定义、主题感知,并与窗口的非客户区(当前 Windows 风格的顶部右侧“=x=”图标)的窗口关闭元素视觉关联。当它失去焦点或被按下 Esc 键时,它会消失(相当于“取消”),从而在用户未做出选择时安全地保持原样,这可以被认为是一种更柔和、更友好的模态形式。这种行为对于任何意外情况都安全稳定,并且需要更少的用户输入活动。所有操作都可以通过鼠标或键盘,或两者的任何组合来完成。

在此特定应用程序中,如果未加载输入图像,则不为主窗口请求操作选择。对于预览窗口,如果用户以位图格式和至少一种文本格式(纯文本或 HTML)保存了图像,则不请求选择。因此,在关闭菜单中不显示保存文件的选项,但此选项可以在其他应用程序中使用。

此样式和行为的实现足够棘手且有趣,可能需要单独一篇文章来介绍。

此想法的当前实现虽然相当可靠,但对于应用程序开发人员来说,仍然不够通用、可定制或方便,因此可以认为它是一个工作原型。有关更多详细信息,请参阅源文件“ClosingWindowControl.*”和“TheApplication.cs”。

应用程序和窗口图标

不幸的是,默认的 C# 项目模板实际上鼓励了不良的开发实践:应用程序图标与窗口图标是独立创建的。即使图标相同,大多数开发人员也不会重用它们,这是完全错误的,因为它违反了实际中非常重要的单一真相原则 (SPOT)。没有明显的方法可以使用单个源图标达到这两个目的。

在当前应用程序中,通过使用我自己的 WPF 项目模板,我演示了一个针对此问题的已知简单可靠的解决方案。

首先,开发人员需要创建一个或多个图标文件。该文件可以包含在 .resx 文件中,并作为资源用于一个或多个对象、某些窗口或应用程序(我强烈建议使用第三方工具开发图标,并将其作为单独的文件包含在项目中,通过“添加现有文件…”选项使用。)

该资源的第一位使用者应该是应用程序。要将嵌入应用程序的可重用资源作为应用程序图标,我们需要一个显式的入口点方法 (Main)。有关在 WPF 应用程序中需要此方法的环境,请参阅源文件“TheApplication.cs”。入口点方法可能如下所示:

[STAThread]
static void Main(string[] args) {
    using (var iconStream = new System.IO.MemoryStream()) {
        TheApplication app = new TheApplication();
        UnicodeArt.Resources.Resources.IconMain.Save(iconStream);
        iconStream.Seek(0, System.IO.SeekOrigin.Begin);
        app.ApplicationIcon =
            System.Windows.Media.Imaging.BitmapFrame.Create(iconStream);
        app.Run();
    } //using
} //Main

此代码片段假定文件“Resources.xres”已添加到项目目录资源中,并且默认命名空间为“UnicodeArt”,它会生成上面显示的资源变量的自动生成名称。要获取相应图标的名称,只需在自动生成的文件(在本例中为“Resources.Designer.cs”)中查找它。

假设至少有一个图标,即主窗口的图标,应该与应用程序图标相同,我在TheApplication类中创建应用程序窗口图标时为其赋值。

protected override void OnStartup(StartupEventArgs e) {
    // ...
    MainWindow = new Ui.MainWindow();
    MainWindow.Title = ProductName;
    MainWindow.Icon = ApplicationIcon;
    MainWindow.Show();
    // ...
} //OnStartup

如果其他窗口需要相同的图标,它们可以从主窗口复制引用。或者,可以将其视为TheApplication.Current.ApplicationIcon,假设TheApplication.Current是类似于Application.CurrentTheApplication单例,而ApplicationIcon是该类型的属性。

兼容性和构建

由于代码基于 WPF,我使用了第一个与 WPF 兼容性较好的平台版本——Microsoft.NET v.3.5。相应地,我为 Visual Studio 2008 提供了一个解决方案和项目。我这样做是故意的,目的是覆盖所有可能使用 WPF 的读者。稍后的 .NET 版本将得到支持;稍后的 Visual Studio 版本可以自动升级解决方案和项目文件。

事实上,构建不需要 Visual Studio。代码可以通过提供的批处理文件“build.bat”进行批处理构建。如果您的 Windows 安装目录与默认目录不同,构建仍会成功。如果 .NET 安装目录与默认目录不同,请参阅此批处理文件的内容及其第一行中的注释——下一行可以修改以进行构建。

结论之外:阿列夫数

为什么应用程序图标是希伯来字母阿莱夫,ℵ?

首先,这个字母继承自腓尼基'Ālep,象征着字母表和“alphabet”这个词的概念,它通过希腊语 ἀλφάβητος,源自两个腓尼基字母,'ĀlepBēt,可以追溯到腓尼基人,甚至更早。因此,“alphabet”一词可以解释为“牛的房子”:-)。

此外,在数学中,这个字符表示阿莱夫数,代表无限集基数。它暗示了“无限”数量的可能 Unicode 艺术作品的构想,但这只是比喻上的说法:在计算机上进行的一切总是某种有限集,仅仅因为任何计算机都只是一个有限状态机:-)。

我将乐意回应各种信息性的批评,并考虑对此主题的建议。

许可说明

此外,还有Code Project 开源许可

所有图像均为原创,由文章作者从零开始创建。

同时,某些图像中使用的字符字形可能受所用字体开发者的版权保护。最终,专有版权字体的所有者可能会向本应用程序的任何用户以及任何文本渲染作品(印刷或可印刷的书籍、文章、应用程序 UI、财务报告等)的作者索要版税支付。:-)

© . All rights reserved.