The preface

In the old article “How to Write a Command Line Stopwatch,” with the command tput, I achieved the effect of “in place update” output minutes and seconds

The ASCII escape sequences \x1b[8D and \x1b[0K are used. In addition, the ASCII escape sequences have many other functions. For example, they can be used to customize the foreground color of the output

Change the parameter 38 in the escape sequence to 48 to customize the background color of the output content

Changing the print to two Spaces makes it look like a red square on a black canvas

In this case, as long as the size is suitable, you can print a picture from the terminal by using the color of each pixel as the background color and two Spaces on the row and column corresponding to the coordinates. You can even animate it if you can erase the output and print a different picture in the same place.

Seeing is believing. Let me demonstrate this in Python.

Load the GIF into the terminal

To display a GIF image in the terminal using the above idea, you must first get the color of each pixel in each frame of the GIF image. GIF files can be easily parsed in Python using a library called Pillow, which you install first

➜ / 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.2MB) Installing Installation 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.Copy the code

You can then ask it to read in and parse a GIF image

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
Copy the code

Each frame is then converted to RGB mode and traversed through each pixel

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
Copy the code

Calling the Image instance method Load yields an instance of the PixelAccess class, which retrieves the color value of each pixel using coordinates like a two-dimensional array. The color value is a tuple of length 3, which in turn are the components of the pixel’s primary colors.

From the 24-bit section of the ANSI Escape Code entry, the use parameter is 48; 2; , followed by the three primary color components separated by a semicolon to set the 24-bit background color

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(' ')
Copy the code

After each double loop through all the pixels, you must also clear the output and reset the cursor to the top left corner before printing again, which can be done using an ASCII escape sequence. As shown in the VT100 User Guide, the ED command erases the displayed characters by the \x1b[2J; the CUP command moves the cursor to the upper left corner by the \x1b[0;0H; output the two escape sequences before each frame is printed

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(' ')
Copy the code

Finally, you just need to go to sleep one frame at a time as required by the GIF file. The display duration of each frame can be obtained in milliseconds from the key duration in the INFO property

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)
Copy the code

Now you can see the effect. I prepared a test GIF of 47 pixels in width and height for 34 frames

Let it show up in the terminal

A slight improvement

You may have noticed that there is a noticeable flicker in the previous demo, which is caused by the inability to print the ASCII escape sequence fast enough. In this case, a whole line of escape sequences can be generated and printed to the terminal once more. Changes are not complicated

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]
                ifcolors ! = 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)
Copy the code

But the effect is significant

The full text after

Read the original