序言
在旧文《如何写一个命令行的秒表》中,借助命令tput
,我实现了“原地更新”所输入的时分秒的成果
其中用到的是 ASCII 转义序列 \x1b[8D
和\x1b[0K
。除此之外,ASCII 转义序列还有许多其它性能。例如,能够用来定制输入内容的前景色
将转义序列中的参数 38
改为48
,能够定制输入内容的背景色
将打印内容改为两个空格,看起来就像是在一块彩色的画布上涂了一个红色的方块
既然如此,只有尺寸适合,就能够在终端打印出一张图片,只须要将每一个像素的色彩作为背景色,在坐标对应的行列上输入两个空格即可。如果能抹掉输入的内容并在同样的地位上打印一张不同的图片,甚至能够实现动画的成果。
百闻不如一见,上面我用 Python 演示一番。
把 GIF 装进终端
要想用前文的思路在终端中显示一张 GIF 图片,必须先失去 GIF 图片每一帧的每个像素的色彩才行。在 Python 中应用名为 Pillow 的库能够轻松地解析 GIF 文件,先装置这个库
➜ /tmp rmdir show_gif
➜ /tmp mkdir show_gif
➜ /tmp cd show_gif
➜ show_gif python3 -m venv ./venv
➜ show_gif . ./venv/bin/activate
(venv) ➜ show_gif pip install Pillow
Collecting Pillow
Using cached Pillow-8.1.0-cp39-cp39-macosx_10_10_x86_64.whl (2.2 MB)
Installing collected packages: Pillow
Successfully installed Pillow-8.1.0
WARNING: You are using pip version 20.2.3; however, version 21.0.1 is available.
You should consider upgrading via the '/private/tmp/show_gif/venv/bin/python3 -m pip install --upgrade pip' command.
接着便能够让它读入并解析一张 GIF 图片
import sys
from PIL import Image, ImageSequence
if __name__ == '__main__':
path = sys.argv[1]
im = Image.open(path)
for frame in ImageSequence.Iterator(im):
pass
而后将每一帧都转换为 RGB
模式再遍历其每一个像素
import sys
from PIL import Image, ImageSequence
if __name__ == '__main__':
path = sys.argv[1]
im = Image.open(path)
for frame in ImageSequence.Iterator(im):
rgb_frame = frame.convert('RGB')
pixels = rgb_frame.load()
for y in range(0, rgb_frame.height):
for x in range(0, rgb_frame.width):
pass
调用 Image
类的实例办法 load
失去的是一个 PixelAccess
类的实例,它能够像二维数组个别用坐标获取每一个像素的色彩值,色彩值则是一个长度为 3 的 tuple
类型的值,其中顺次是像素的三原色的重量。
从 ANSI escape code 词条的 24-bit 大节中得悉,应用参数为 48;2;
的转义序列,再接上以分号分隔的三原色重量即可设置 24 位的背景色
import sys
from PIL import Image, ImageSequence
if __name__ == '__main__':
path = sys.argv[1]
im = Image.open(path)
for frame in ImageSequence.Iterator(im):
rgb_frame = frame.convert('RGB')
pixels = rgb_frame.load()
for y in range(0, rgb_frame.height):
for x in range(0, rgb_frame.width):
colors = pixels[x, y]
print('\x1b[48;2;{};{};{}m \x1b[0m'.format(*colors), end='')
print('')
在每次二重循环遍历了所有像素后,还必须革除输入的内容,并将光标重置到左上角能力再次打印,这能够用 ASCII 转义序列来实现。查阅 VT100 User Guide 能够晓得,用 ED 命令能够擦除显示的字符,对应的转义序列为\x1b[2J
;用 CUP 命令能够挪动光标的地位到左上角,对应的转义序列为\x1b[0;0H
。在每次开始打印一帧图像前输入这两个转义序列即可
import sys
from PIL import Image, ImageSequence
if __name__ == '__main__':
path = sys.argv[1]
im = Image.open(path)
for frame in ImageSequence.Iterator(im):
rgb_frame = frame.convert('RGB')
pixels = rgb_frame.load()
print('\x1b[2J\x1b[0;0H', end='')
for y in range(0, rgb_frame.height):
for x in range(0, rgb_frame.width):
colors = pixels[x, y]
print('\x1b[48;2;{};{};{}m \x1b[0m'.format(*colors), end='')
print('')
最初,只须要在每次打印完一帧后,按 GIF 文件的要求睡眠一段时间即可。每一帧的展现时长能够从 info
属性的键 duration
中失去,单位是毫秒
import sys
import time
from PIL import Image, ImageSequence
if __name__ == '__main__':
path = sys.argv[1]
im = Image.open(path)
for frame in ImageSequence.Iterator(im):
rgb_frame = frame.convert('RGB')
pixels = rgb_frame.load()
print('\x1b[2J\x1b[0;0H', end='')
for y in range(0, rgb_frame.height):
for x in range(0, rgb_frame.width):
colors = pixels[x, y]
print('\x1b[48;2;{};{};{}m \x1b[0m'.format(*colors), end='')
print('')
time.sleep(rgb_frame.info['duration'] / 1000)
当初能够看看成果了。我筹备了一张测试用的 GIF 图片,宽度和高度均为 47 像素,共 34 帧
让它在终端中显示进去吧
一点渺小的改良
你可能留意到了,前文的演示成果中有显著的闪动,这是因为打印 ASCII 转义序列的速度不够快导致的。既然如此,能够将一整行的转义序列学生成进去,再一次性输入到终端。改变不简单
import sys
import time
from PIL import Image, ImageSequence
if __name__ == '__main__':
path = sys.argv[1]
im = Image.open(path)
for frame in ImageSequence.Iterator(im):
rgb_frame = frame.convert('RGB')
pixels = rgb_frame.load()
print('\x1b[2J\x1b[0;0H', end='')
for y in range(0, rgb_frame.height):
last_colors = None
line = ''
for x in range(0, rgb_frame.width):
colors = pixels[x, y]
if colors != last_colors:
line += '\x1b[0m\x1b[48;2;{};{};{}m'.format(*colors)
else:
line += ' '
last_colors = colors
print('{}\x1b[0m'.format(line))
time.sleep(rgb_frame.info['duration'] / 1000)
但成果却很显著
全文完
浏览原文