估计阅读时长: 12 分钟
https://github.com/xieguigang/Microsoft.VisualBasic.Drawing

最近在Linux服务器上面搞数据分析,因为Linux服务器只能够是通过SSH远程登陆上去的,没有图形化界面,所以想查看生成的结果图的话,只能够将图片文件通过FileZilla工具从服务器上下载下来在本地查看。这种方法非常的繁琐,至少相对于在服务器上跑完了程序后直接查看结果这样子的操作要复杂一些。

如果要能够直接在Linux服务器上查看图片,可行的一个方法就是,如果你有服务器的Root权限的话,可以将你的目录通过smb协议共享出来,在windows上挂在为共享文件夹,这样子在Linux服务器上跑完命令后,再回到Windows的Explorer程序上刷新一下。但是这个对于网络地理位置较远的服务器而言,可能网络速度不是很好,对于几十兆的图片结果文件,可能刷新会存在延迟,你可能需要刷新好几次才会更新Windows上的图片缩略图;并且通过smb开放共享文件夹你还需要记住smb的第二套账号密码,如果账号密码过于简单,那么你的Linux服务器上的数据安全性就会存在问题。

另一个方案就是通过SSH-FS方案,通过你的ssh账号将远程Linux服务器挂载为本地硬盘,来查看服务器上生成的图片文件。但是这个也和上面的方案一样会受限于网络传输速度的影响。

看来,我们只能够在Linux的终端上想办法来进行图片文件的查看了。

这个时候,可能你就会惊呼了,这怎么可能,我们通过ssh远程上去的Linux终端就是一个纯文本组成的命令行,怎么可能直接显示图片呢。只要思想不滑坡,办法总是有的。可能你之前会了解过通过ASCII Art的方式在Linux终端上显示图像:对于ASCII Art方式,我们会将不同像素点的亮度信息(或者说灰度信息)映射到占据不同显示面积的字符上,从而组成了一副可以显示灰度差异的黑白字符画。这个方法可以解决我们的一部分显示需求,但是不多。

现在,假若我们需要在Linux终端上显示出彩色的图像的话,则会需要使用到终端上的一个字符显示的样式控制特性了:ANSI转义序列。ANSI 转义序列(ANSI Escape Sequences)是一种用于控制终端(如命令行界面、文本编辑器等)显示效果的编码方式。它们主要用于设置文本的颜色、样式(如加粗、斜体等)等终端的显示效果。通过组合使用这些序列,可以实现丰富的文本显示效果。

ANSI转义序列的基础知识

基本结构

ANSI 转义序列通常以 ESC 字符(ASCII 码 27,表示为 \033\x1B)开头,后面跟着方括号 [,然后是若干参数,最后是一个控制字符(如 m 表示设置文本属性)。
例如:

  • \033[31m:设置文本颜色为红色
  • \033[1m:设置文本为加粗
  • \033[2J:清除屏幕

常用转义序列

  1. 文本颜色 前景色(文本颜色):\033[30m:黑色, \033[31m:红色, \033[32m:绿色, \033[33m:黄色, \033[34m:蓝色, \033[35m:洋红, \033[36m:青色, \033[37m:白色。背景色:\033[40m:黑色, \033[41m:红色, \033[42m:绿色, \033[43m:黄色, \033[44m:蓝色, \033[45m:洋红, \033[46m:青色, \033[47m:白色
  2. 文本样式: \033[1m:加粗, \033[2m:斜体, \033[3m:闪烁, \033[4m:下划线, \033[5m:闪烁(更频繁), \033[7m:反向显示(交换前景色和背景色), \033[8m:隐藏文本
  3. 光标控制: \033[2J:清除屏幕, \033[1J:清除从光标位置到屏幕末尾的内容, \033[?25h:显示光标, \033[?25l:隐藏光标
  4. 颜色扩展: \033[38;2;r;g;bm:设置前景色为 RGB 值, \033[48;2;r;g;bm:设置背景色为 RGB 值

当然,我们可以将上面所提到的样式标记相互将多个 ANSI 转义序列组合使用,例如:

  • \033[1;32m:加粗绿色文本
  • \033[44;37m:蓝色背景,白色文本

以下是一个简单的示例,展示了如何使用 ANSI 转义序列:

  • \033[31mHello, World!\033[0m:红色显示 "Hello, World!"
  • \033[1;32;40mHello, World!\033[0m:加粗绿色文本,黑色背景

最后,我们可以通过转义\033[0m重置所有属性到默认值。由于我们使用的不同的终端程序的开发年代不一样,所以可能会出现不同的终端可能对某些 ANSI 转义序列的支持程度不同。当然这也和终端程序的开发目标有关,有些轻量化的终端可能压根就不支持这种特性,这时候将ANSI转义序列打印在终端上,我们得到的只会是一团乱码。

基于ANSI转义序列显示图片

从上面的ANSI转义序列基础知识上可以了解到,ANSI转义序列是支持rgb颜色的!假若我们所使用的命令行终端也支持这一rgb颜色特性,那么我们就可以在我们的命令行终端上进行图片的显示了。通过前面的ASCII Art知识了解到,一个字符是可以作为一个像素点来使用。在这里基于ANSI转义序列的方法,同样也是一个字符作为一个像素点来使用。在这里,我们使用一个<space>空白符号来作为一个像素点显示。当我们针对每一个像素点基于ANSI转义序列添加上对应的rgb颜色之后,就可以将命令行上的不同的空白符显示出不同的颜色。

graph TD
    A[加载图像文件] --> B[转换为RGB格式]
    B --> C{终端宽度探测}
    C -->|用户指定| D[使用指定宽度]
    C -->|未指定| E[获取终端列数]
    D/E --> F[按比例计算高度]
    F --> G[图像缩放]
    G --> H{颜色模式}
    H -->|真彩色| I[RGB直接映射]
    H -->|256色| J[RGB转ANSI索引]
    I/J --> K[生成ANSI序列]
    K --> L[输出到终端]

下面就是我们在这里通过ANSI转义序列进行图片显示的算法流程:

  1. ANSI 转义序列:
    • 使用 \x1b[48;5;{color}m 设置背景色(256 色模式),或者也可以在支持rgb颜色的终端程序上
    • 使用 \x1b[48;2;{r};{g};{b}m 设置真彩色背景(24 位 RGB)
    • 用空格字符显示颜色块(每个空格对应一个像素)
    • 每行结束时用 \x1b[0m\n 重置样式并换行
''' <summary>
''' ANSI 转义字符
''' </summary>
Public Const Escape As String = ChrW(&H1B)
  1. GDI+图像处理工作流程
    • 使用 System.Drawing.Image 加载图像
    • 根据终端宽度(Console.WindowWidth或者用户手动设定显示宽度字符数量)计算缩放高度(保持宽高比)
    • 使用高质量双三次插值缩放图像到终端显示的字符区域大小
' 将像素点颜色编码为终端上的一个彩色空白字符
Private Function GetAnsiColorSequence(color As Color) As String
    ' 使用真彩色 ANSI 序列 (24-bit RGB)
    Return $"{AnsiEscapeCodes.Escape}[48;2;{color.R};{color.G};{color.B}m"
End Function

' 备用:256 色模式 (当终端不支持真彩色时)
Private Function GetAnsi256ColorSequence(color As Color) As String
   Dim index As Integer = RgbToAnsi256(color.R, color.G, color.B)
   Return $"{AnsiEscapeCodes.Escape}[48;5;{index}m"
End Function

Private Function RgbToAnsi256(r As Byte, g As Byte, b As Byte) As Byte
    ' 转换为 ANSI 256 色索引
    If r = g AndAlso g = b Then
        ' 灰度处理
        If r < 8 Then Return 16
        If r > 248 Then Return 231
        Return CByte(231 + (r - 8) / 10)
    Else
       ' RGB 立方体映射 (6x6x6)
       Return CByte(16 +
           (CInt(r / 51) * 36) +
           (CInt(g / 51) * 6) +
           CInt(b / 51))
    End If
End Function
  1. ANSI 转义序列编码
    • 针对缩放后的图片Image对象,转化为内存Bitmap,按照行进行GetPixel操作读取Color数据
    • 将Color转换为当前的像素点ANSI转义序列
    • 使用StringBuilder进行高性能的ANSI转义序列的构建

最后,完成了图片上面的所有像素点的颜色信息扫描之后,将StringBuilder中的所有字符串一次性的通过Console.WriteLine方法输出到Linux终端之上,即可完成图片的打印。下面的VB.NET函数就展示了上面所描述的整个图像扫描和ANSI转义序列的编码过程:

''' <summary>
''' previews of the image on the terminal using ANSI escape codes.
''' </summary>
''' <param name="img"></param>
''' <param name="terminalWidth"></param>
''' <returns>This function generates a string that represents the image in 
''' a format suitable for terminal display.
''' </returns>
<Extension>
Public Function GenerateImagePreview(img As Image, terminalWidth As Integer) As String
    ' 计算新尺寸 (保持宽高比)
    Dim aspectRatio As Single = img.Height / CSng(img.Width)
    Dim newHeight As Integer = CInt(terminalWidth * aspectRatio) * 0.8
    Dim scaledImg As Bitmap

    ' 创建缩放后的位图
    Using gfx As New Graphics(terminalWidth, newHeight)
        gfx.DrawImage(img, 0, 0, terminalWidth, newHeight)
        gfx.Flush()
        scaledImg = DirectCast(gfx.ImageResource, SkiaImage).ToBitmap
    End Using

    ' 生成 ANSI 序列
    Dim ansiBuilder As New StringBuilder()
    Dim pixel As Color

    For y As Integer = 0 To scaledImg.Height - 1
        For x As Integer = 0 To scaledImg.Width - 1
            pixel = scaledImg.GetPixel(x, y)
            ' 空格作为像素显示
            ansiBuilder.Append(If(useTrueColor, GetAnsiColorSequence(pixel), GetAnsi256ColorSequence(pixel)))
            ansiBuilder.Append(" ")
        Next
        ' 重置样式并换行
        ansiBuilder.AppendLine(AnsiEscapeCodes.Escape & "[0m")
    Next

    Return ansiBuilder.ToString()
End Function

接着就可以构建R#函数,用于在R#脚本编程之中进行调用了:

''' <summary>
''' The R# Graphics Devices and Support for Colours and Fonts
''' </summary>
<Package("grDevices", Category:=APICategories.UtilityTools)>
Public Module grDevices

    ''' <summary>
    ''' Display images in ansii escape sequences
    ''' </summary>
    ''' <param name="image">
    ''' the clr image object or a file path of the image file.
    ''' 
    ''' If this parameter is a file path, then the image will be loaded
    ''' from the given file path, otherwise, if this parameter is a clr
    ''' image object, then this function will try to display it in the
    ''' ansii escape sequences.
    ''' </param>
    ''' <param name="env"></param>
    ''' <returns></returns>
    ''' <example>
    ''' grDevices::display("https://raw.githubusercontent.com/graphics/graphics.png");
    ''' grDevices::display("/Users/user/Pictures/image.png");
    ''' 
    ''' # display a clr image object
    ''' grDevices::display(image);
    ''' </example>
    <ExportAPI("display")>
    Public Function display(image As Object,
                            Optional width As Integer? = Nothing,
                            Optional c256_color As Boolean = False,
                            Optional env As Environment = Nothing) As Object

        If image Is Nothing Then
            Return False
        End If
        If width Is Nothing OrElse width <= 0 Then
            width = Console.WindowWidth - 1
        End If

        If TypeOf image Is String Then
            Dim filePath As String = NetFile.GetMapPath(DirectCast(image, String))

            If Not filePath.FileExists Then
                Return RInternal.debug.stop(New FileNotFoundException(filePath), env)
            Else
                image = Microsoft.VisualBasic.Imaging.Image.FromFile(filePath)
            End If
        End If

        If TypeOf image Is Image Then
            If c256_color Then
                ANSI.useTrueColor = False
            End If

            Call Console.WriteLine(ANSI.GenerateImagePreview(DirectCast(image, Image), width))
        Else
            Return Message.InCompatibleType(GetType(Image), image.GetType, env)
        End If

        Return True
    End Function
End Module

在R#脚本之中可以通过下面的方式进行调用:

grDevices::display("https://raw.githubusercontent.com/graphics/graphics.png");
grDevices::display("/Users/user/Pictures/image.png");

# display a clr image object
grDevices::display(image);

看得出来在Linux终端上显示出来的是几朵黄色的郁金香花么?

黄色郁金香原图

可以看到我们的代码可以通过ANSI转义序列正确的在Linux终端上显示出了图片。但是有一个很大的非常明显的缺点嘛,就是显示出来的图片分辨率实在是太低了,仅能够用作为快速的进行缩略图预览的作用。

谢桂纲
Latest posts by 谢桂纲 (see all)

Attachments

  • Capture • 269 kB • 0 click
    2025年7月26日

  • 853127 • 137 kB • 0 click
    2025年7月26日

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *

博客文章
July 2025
S M T W T F S
 12345
6789101112
13141516171819
20212223242526
2728293031  
  1. […] 这个时候,可能你就会惊呼了,这怎么可能,我们通过ssh远程上去的Linux终端就是一个纯文本组成的命令行,怎么可能直接显示图片呢。只要思想不滑坡,办法总是有的。可能你之前会了解过通过ASCII Art的方式在Linux终端上显示图像:对于ASCII Art方式,我们会将不同像素点的亮度信息(或者说灰度信息)映射到占据不同显示面积的字符上,从而组成了一副可以显示灰度差异的黑白字符画。这个方法可以解决我们的一部分显示需求,但是不多。 […]