iTerm2 has a great but underrated feature. It can display images in the terminal using the “Inline Images Protocol”. There are a few examples of CLI tools that use this feature, like imgls, which lists files with thumbnails of images, or imgcat, which lets you preview images in the terminal. You can also use this feature in your own applications. I used it a while ago to quickly analyse the output of my DSP algorithms (you can find the source code on my GitLab page).

A screenshot with iTerm2 terminal displaying application with embedded image

To do this, you need to encode an image in base64 and put it in the console output in a special escape sequence. According to the docs, this feature allows you to use any image format supported by MacOS, including PNG, GIF, PDF, etc. You can read more about this in the iTerm2 docs: https://iterm2.com/documentation-images.html.

Skia

Skia is a library for rendering 2D vector images in C++. It is developed by Google and is used in their various products such as Chrome, ChromeOS, Android and Flutter, as well as by others such as Firefox and LibreOffice, so we can expect that Google won’t kill this project too quickly (but who knows). Skia provides a Canvas-like API, so it’s easy to use if you’re familiar with it.

Dependencies

In the following example, I’m going to render and display a simple horizontal line. The project uses two dependencies:

If you are using vcpkg, you can add dependencies to your vcpkg.json:

	"dependencies": [
		"skia",
		"cppcodec"
	]

Rendering an image

First we need to include the Skia headers that we will use in the next steps:

#include "skia/core/SkImage.h"
#include "skia/core/SkStream.h"
#include "skia/core/SkPaint.h"
#include "skia/core/SkData.h"
#include "skia/core/SkSurface.h"
#include "skia/core/SkCanvas.h"
#include "skia/core/SkPath.h"

Preparing a canvas

The canvas is a “drawing context”, a place where all our drawings are stored. In this example we will create the canvas using a surface. By using a surface, our image will be rendered into a raster image. If you’d like to use a different backend, see the Skia docs https://skia.org/docs/user/api/skcanvas_creation/.

sk_sp<SkSurface> surface;
SkCanvas* canvas;

surface = SkSurface::MakeRasterN32Premul(width, height);
canvas = surface->getCanvas();

Drawing a path

Now we’re going to create a path with our horizontal line.

SkPath path;

path.moveTo(0, height / 2);
path.lineTo(width, height / 2);

When the path is ready, we can draw it on the canvas. To do this, we first need to create a Paint instance that contains the style of our character (including colour and line width).

SkPaint paint;

paint.setColor(SK_ColorWHITE);
paint.setAntiAlias(true);
paint.setStyle(SkPaint::kStroke_Style);
paint.setStrokeWidth(1);

canvas->translate(0, ((float)height / 2));
canvas->scale(1, 1.0f/maxInWholeSignal);
canvas->drawPath(path, paint);

Rastering the image

Now we can render our image. We’ll use the PNG format because it can handle the transparency layer.

sk_sp<SkImage> img(surface->makeImageSnapshot());

if (!img) {
	std:err << "Cannot make image snapshot\n";

	return 1;
}

sk_sp<SkData> png(img->encodeToData());

if (!png) {
	std:err << "Cannot create PNG image\n";

	return 1;
}

Putting the image in the terminal

Finally, we can put the image in the terminal. First, we need to encode the image to base64. Then put the following escape sequence in the output:

ESC ] 1337 ; File = [parameters] : [image] ^G

Now, let’s put it together:

#include <cppcodec/base64_rfc4648.hpp>

// Draw an image in a terminal
std::string encodedPng = base64::encode(
	(const uint8_t*) png->data(),
	png->size()
);

std::cout << "\033]1337;File=inline=1;preserveAspectRatio=1:"
	<< encodedPng << "\a\n";

What next

Unfortunately, Inline Images Protocol is only supported by a few terminal emulators. If you want to use this feature in your app, it’d be a good idea to check if it’s supported.

If you want to provide a similar function that works with more terminal emulators, you might want to check out Sixel. Sixel is a pretty old format, but it’s supported by most modern terminals. I’ll write about it in a future post.