2ascii:将 JPG、PNG、SVG 和文本渲染为 ASCII





5.00/5 (15投票s)
从常见的图像格式和文本创建 ASCII 艺术

引言
我编写这个工具是为了测试我的图形库与微软最新的 C++ 编译器一起工作的情况。事实证明这并不算什么大事,但我留下了一个有趣的命令行小工具,它可以接受图像或字体和一些文本,并输出 ASCII 艺术。
更新:增加了文本输出支持
必备组件
- 您需要安装 VS Code 并安装 Microsoft CMake 和 C++ 扩展。
- 您需要从 git-scm.org 安装 git 并将其添加到您的路径中。
- 您需要一个 C++ 编译器。我已用 GCC 和 MSVC 测试过。Clang 应该 可以工作,但我很久没有用它运行代码了。
构建 2ascii.exe
在根目录中运行 fetch_deps.cmd,然后在 VS Code 中右键单击 CMakeLists.txt,然后单击构建所有项目。最后,run.cmd 设置为 MSVC Debug 构建,并将使用默认参数运行项目。否则,请手动运行 2ascii.exe。
使用 2ascii.exe
对于图像
2ascii.exe 将文件名作为第一个参数,可选的第二个参数是一个介于 1 到 1000 之间的整数,表示原始图像的缩放比例(百分比)。然后,它将生成的 ASCII 输出到 stdout。
对于文本
2ascii.exe 将 TTF 或 OTF 字体文件名作为第一个参数,行高作为第 2 个参数,文本作为第 3 个参数 - 最好将文本放在双引号中。然后,它将生成的 ASCII 输出。所有参数都是必需的。
免责声明:我用来实现魔法的库是为物联网和嵌入式设备设计的,因此它可能无法处理所有可能的字体或图像。例如,JPG 有许多不同的格式,而该库仅支持常见格式。SVG 和字体也面临类似的挑战。话虽如此,它应该适用于许多文件。如果它不适用于您的 JPG,一个解决方法是先在 mspaint 中打开它,然后再次将其另存为 JPG。
编写这个混乱的程序
所有的魔法都在 main.cpp 中。
首先,我们包含一些头文件。尽管这是 C++,但图形库是为物联网/嵌入式设备设计的,并且目标平台对 STL 的实现不完整/不兼容。此外,小型设备没有足够的 RAM 来有效利用 STL,而且还需要解决堆碎片问题。这就是为什么您会看到 C 头文件而不是 C++ 头文件。
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <gfx.hpp>
using namespace gfx;
您会注意到,除了标准头文件外,我们还包含了 gfx.hpp 并导入了 gfx 命名空间。这就是 htcw_gfx 或 GFX - 执行繁重工作的图形库。
接下来,我们有一个例程,可以将 GFX“绘制源”作为 ASCII 打印到控制台。GFX 绘制源基本上是任何可以提供对其包含的像素数据的随机访问的内容,例如位图。GFX 可以轻松完成此操作。这里的想法是,我们从上到下、从左到右遍历绘制源。在每个点,我们读取该位置像素的颜色值。绘制源的像素格式在此例程中称为 Source::pixel_type。每个绘制目标(源或目标)都公开特定的成员,pixel_type 是其中之一。dimensions() 是另一个。绘制源还公开 point() 来读取像素。这都利用了这些。总之,我们使用 convert<>() 函数将源像素转换为 4 位灰度,生成介于 0 和 15 之间的值,仅包含其 Luminosity 通道。然后,我们将该通道值用作索引,从包含我们“颜色表”的字符串中选择字符。颜色表只是一系列字符,它们越来越“暗”(黑底白字)或“亮”(白底黑字)。目前,它是字符串:“ .,-~;+=x!1%$O@#”。请注意,有 16 个字符(包括开头的空格)。每次增加 y 时,我们都会写入一个换行符。
// prints a source as 4-bit grayscale ASCII
template <typename Source>
void print_ascii(const Source& src) {
    // the color table
    static const char* col_table = " .,-~;+=x!1%$O@#";
    // move through the draw source
    for (int y = 0; y < src.dimensions().height; ++y) {
        for (int x = 0; x < src.dimensions().width; ++x) {
            typename Source::pixel_type px;
            // get the pixel at the current point
            src.point(point16(x, y), &px);
            // convert it to 4-bit grayscale (0-15)
            const auto px2 = convert<typename Source::pixel_type, gsc_pixel<4>>(px);
            // get the solitary "L" (luminosity) channel value off the pixel
            size_t i = px2.template channel<channel_name::L>();
            // use it as an index into the color table
            putchar(col_table[i]);
        }
        putchar('\r');
        putchar('\n');
    }
}
在 main() 中,我们首先检查参数并解析第 2 个参数。
if (argc > 1) {       // at least 1 param
    float scale = 1;  // scale of image
    if (argc > 2) {   // 2nd arg is scale percentage
        int pct = atoi(argv[2]);
        if (pct > 0 && pct <= 1000) {
            scale = ((float)pct / 100.0f);
        }
    }
此时,我们的 scale 反映了输入的百分比,并被缩放到一个浮点值,其中 1 表示 1:1 缩放,.5 表示 1:2 缩放。
现在,我们打开 argv[1] 中命名的文件,并获取其长度,我们稍后会用到。我们还准备了一些标志。最后,我们确保文件名比 4 个字符长(包括 . 和扩展名)。
// open the file
file_stream fs(argv[1]);
size_t arglen = strlen(argv[1]);
bool png = false;
bool jpg = false;
if (arglen > 4) {
如果它是 SVG,我们使用 GFX 从 file_stream 创建并读取一个 svg_doc。然后,我们创建一个位图,其大小为缩放后的最终输出尺寸。接下来,我们将 SVG 按指定比例绘制到位图中,然后将该位图打印为 ASCII。最后,我们释放位图并 return 0,表示成功。
if (0 == stricmp_i(argv[1] + arglen - 4, ".svg")) {
    svg_doc doc;
    // read it
    svg_doc::read(&fs, &doc);
    fs.close();
    // create a bitmap the size of our final scaled SVG
    auto bmp = create_bitmap<gsc_pixel<4>>(
        {uint16_t(doc.dimensions().width * scale),
            uint16_t(doc.dimensions().height * scale)});
    // if not out of mem allocating bitmap
    if (bmp.begin()) {
        // clear it
        bmp.clear(bmp.bounds());
        // draw the SVG
        draw::svg(bmp, bmp.bounds(), doc, scale);
        // dump as ascii
        print_ascii(bmp);
        // free the bmp
        free(bmp.begin());
        return 0;
    }
    return 1;
否则,如果它是 JPG 或 PNG,我们会设置相应的标志。
} else if (0 == stricmp_i(argv[1] + arglen - 4, ".jpg")) {
    jpg = true;
} else if (0 == stricmp_i(argv[1] + arglen - 4, ".png")) {
    png = true;
}
如果是 JPG 或 PNG,代码基本上相同,因此它依赖于相同的处理代码。对于 scale 为 1 的情况,我们只需创建一个与图像大小相同的位图,将 image draw 到其中,然后 free() 位图后再返回。如果比例不是 1,我们就必须做额外的工作。首先,我们分配一个具有最终缩放尺寸的位图。然后,我们分配另一个与图像大小相同的位图。我们将图像绘制到第二个位图中,然后将其重采样到第一个位图中。如果图像较大,我们使用线性重采样。如果图像较小,我们使用三次样条重采样。最后,我们将图像打印为 ASCII,释放位图并 return 0,表示成功。
int result = 1;
size16 dim;
if (gfx_result::success == 
        (jpg ? jpeg_image::dimensions(&fs, &dim) 
            : png_image::dimensions(&fs, &dim))) {
    fs.seek(0);
    auto bmp_original = create_bitmap<gsc_pixel<4>>(
        {uint16_t(dim.width),
            uint16_t(dim.height)});
    if (bmp_original.begin()) {
        bmp_original.clear(bmp_original.bounds());
        draw::image(bmp_original, bmp_original.bounds(), &fs);
        fs.close();
        if (scale != 1) {
            // create a bitmap the size of our final scaled image
            auto bmp = create_bitmap<gsc_pixel<4>>(
                {uint16_t(dim.width * scale),
                    uint16_t(dim.height * scale)});
            // if not out of mem allocating bitmap
            if (bmp.begin()) {
                // clear it
                bmp.clear(bmp.bounds());
                // draw the SVG
                if (scale < 1) {
                    draw::bitmap(bmp, 
                                bmp.bounds(), 
                                bmp_original, 
                                bmp_original.bounds(), 
                                bitmap_resize::resize_bicubic);
                } else {
                    draw::bitmap(bmp, 
                                bmp.bounds(), 
                                bmp_original, 
                                bmp_original.bounds(), 
                                bitmap_resize::resize_bilinear);
                }
                result = 0;
                // dump as ascii
                print_ascii(bmp);
                // free the bmp
                free(bmp.begin());
            }
        } else {
            result = 0;
            // dump as ascii
            print_ascii(bmp_original);
        }
        free(bmp_original.begin());
        return result;
    }
}
精明的读者可能会注意到我们的位图是 gsc_pixel<4> 格式。这是 4 位灰度,目的是节省内存,因为我们不需要更高的颜色深度,这样我们就可以每字节打包 2 个像素,而不是在全彩色深度下每像素需要 3 个字节。
基本上就是这样了。希望您觉得这个图形库有用且易于上手。文档在上面提供的链接中。它对于物联网和嵌入式设备来说非常强大,但即使在 PC 上也能带来乐趣。祝您使用愉快!
历史
- 2023 年 10 月 5 日 - 初次提交
- 2023 年 10 月 23 日 - 增加了文本输出


