# terminal-image: putting a real image in a terminal, in full resolution where the terminal allows it

> terminal-image is a small Node package that returns the ANSI escape sequence for displaying an image, detecting at runtime whether the terminal supports the Kitty or iTerm2 graphics protocols and falling back to coloured Unicode half-blocks otherwise. It is a library rather than a command, and its API is four functions and a set of options.

**sindresorhus/terminal-image** — Display images in the terminal

- Repository: https://github.com/sindresorhus/terminal-image
- Stars: 1,133 · Forks: 30
- Language: JavaScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/sindresorhus-terminal-image

## Three renderers, chosen at runtime

terminal-image displays images in the terminal, and the README says it works in any terminal that supports colors. The interesting part is the second sentence: in iTerm, Kitty, WezTerm, and other terminals with graphics support, the image will be displayed in full resolution, and in other terminals it will be rendered using ANSI block characters.

The Terminal Protocol Support section spells out the order it tries:

The Kitty Graphics Protocol gives full resolution support in Kitty, Konsole, WezTerm when configured. The iTerm2 Inline Images Protocol gives full resolution support in iTerm2 and compatible terminals. And ANSI Block Characters is the universal fallback using Unicode half-blocks with 24-bit color.

So there are two entirely different renderers behind one function call. In a graphics-capable terminal, the library emits the protocol's escape sequence and the terminal does the drawing, which means the terminal's own image scaling applies and the result is a real image at whatever the terminal thinks its resolution is. In a plain terminal, the library has to rasterise the image itself into text, and the half-block trick is what makes that look acceptable: a Unicode half-block character is a cell with a foreground colour in the top half and a background colour in the bottom half, so one character can carry two vertical pixels, and with 24-bit colour you can approximate an image with a resolution twice the character grid in height.

The detection is automatic and the option preferNativeRender, a boolean defaulting to true, lets you turn it off to force ANSI rendering. That option exists for a practical reason: a script that generates escape sequences and pipes them somewhere else, or a test that wants deterministic output, needs to be able to choose the fallback path rather than depend on what the terminal happens to be.

The primary language of the package is JavaScript, the licence is MIT, and the repository is not archived. The last push was on 2026-07-09, which is the same day as the v5.0.1 release, and v5.0.0 landed hours earlier on the same day, so the 5.0 major bump was accompanied by a rewrite of the rendering layer rather than an incremental change.

## The API, which is four functions and a promise

The API section is short enough to quote in full, and the brevity is the design.

terminalImage.buffer(imageBuffer, options?) and terminalImage.file(filePath, options?) both return a Promise<string> with the ANSI escape codes to display the image. So the library does not print anything. It gives you a string, and you decide where it goes, which means it composes with any output mechanism you already have rather than insisting on writing to stdout.

```js
import terminalImage from 'terminal-image';

console.log(await terminalImage.file('unicorn.jpg'));
```

Then there are the animated variants, terminalImage.gifBuffer(imageBuffer, options?) and terminalImage.gifFile(filePath, options?), which return a function that can be called to stop the GIF animation. The difference in the return type is the whole distinction between the two pairs: a static image resolves to a string, an animation resolves to a stop function, because an animation has to be started and later torn down.

The file structure makes the split concrete. There is index.js and index.d.ts for the main entry, gif.js as a separate published file, and the package.json files array lists exactly those three as the published contents. There is an example.js, an example-gif.js, a fixture.jpg and a fixture.gif for the tests, plus screenshot.png and screenshot-iterm.jpg for the README.

The module is ESM only. The package type is module, the exports map has types and default entries pointing at index.d.ts and index.js, and there is no require condition, so a CommonJS consumer needs a dynamic import. The engines field requires node >=22, and the test script is xo && node --test, so the linter and the built-in Node test runner are the whole harness. sideEffects is false, so a bundler can drop the module when it is unused.

The dependency list also explains the design. term-img and supports-terminal-graphics are the author's own packages for the rendering and for protocol detection, @sindresorhus/gifkit handles GIF frame extraction, jimp and image-dimensions do the image work, and chalk and log-update are for the colour and for the in-place frame updates during animation.

## Sizing, and why the percentage option is recommended

The options are where the interesting behaviour is, and the README spends a paragraph on the interaction between them.

height and width can each be a string or a number. As a string they are a percentage, and as a number they are a count of rows or columns of the terminal window. The library always scales the image to fit the size of the terminal, so the values you give are a request within a ceiling rather than an absolute size. If width and height are not defined, by default the image will take the width and height of the terminal.

The README recommends the percentage option, and it is worth understanding why rather than taking it on faith. The problem is that a fixed number of columns or rows is measured against the terminal at the moment the image is rendered. Percentages are relative, so the same call produces a proportional result in a narrow window and a wide one. A fixed 70 columns fills a wide terminal and overflows a narrow one, and the overflow is what causes the image to be scaled down anyway, so you end up with a smaller image than you asked for in exactly the case where the terminal is small. A percentage of 50 percent asks for half the available space, which is what you usually mean.

The other option worth noting is preserveAspectRatio, a boolean defaulting to true. By default the aspect ratio is always maintained, and setting it to false lets you stretch the image to exactly the dimensions you asked for, which is occasionally what you want for a chart or a banner and is usually a mistake for a photograph.

```js
import terminalImage from 'terminal-image';

console.log(await terminalImage.file('unicorn.jpg', {width: 70, height: 50, preserveAspectRatio: false}));
```

The GIF-specific options follow the same pattern. maximumFrameRate is a number defaulting to 30 and only works for the GIF functions, and it is ignored when using iTerm. renderFrame is a function taking the frame text and returning nothing, defaulting to log-update, and it is run for each frame of the animation, which is the hook for changing how each frame is shown. renderFrame.done is a function run when the animation playback is stopped, also defaulting to log-update, and it is the place for a cleanup after playback has finished.

## Where terminal-image is the wrong tool

There are three limits, and the first is about what the output actually is.

The first is that the return value is a string of escape codes, not an image file and not a drawing operation. If you want to save a scaled PNG, terminal-image is the wrong tool, because the scaling happens into the character grid, not into pixels. The same applies to pipelines: a program that writes a file, or a CI job that produces an artifact, wants a real image, and the half-block renderer produces text.

The second is the terminal coverage, and the qualifier in the README's first line is doing more work than it appears to. It works in any terminal that supports colors, and the fallback needs Unicode half-block characters and 24-bit colour to look like anything. A terminal with 16 colours will render the fallback as a coarse approximation with visible banding, and one with no colour at all gets nothing useful. There is also the multiplier problem, which the README does not mention and which is worth knowing about: tmux and some screen multiplexers historically did not pass through the Kitty or iTerm2 graphics protocols, so the detection may pick the native path and the terminal inside tmux may not display it. The preferNativeRender option to force ANSI rendering exists for this class of situation, and the fact that the option is documented at all suggests the author has met it.

The third is that the fallback renderer is the weak path, and it is the path most users get. Half-block rendering at 24-bit colour looks like a photograph in a terminal screenshot, and it is genuinely the only way to get a picture out of a text grid. But the fidelity depends on the source image. A photograph with smooth gradients is the hard case, because the two-pixel-per-cell trick relies on the fact that a cell's foreground and background colours are independent, which approximates a vertical pair of pixels exactly but approximates a horizontal pair only as well as the terminal's cell width allows, which is one character for two pixels. Sharp detail and fine text in an image become noise, and the library's own fixture is a jpg and a gif, which are the formats it supports.

The supported formats are worth stating plainly as well: PNG and JPEG, with animated GIFs supported through the separate gifBuffer and gifFile functions. Anything else, a WebP or an SVG, is not in the list, and the README does not describe conversion. The package dependencies include jimp, which can decode more formats than the API exposes, so a caller could convert first, but that is the caller's work.

## terminal-image against the alternatives

There are three practical alternatives, and the difference in approach is mostly about where the rendering happens.

The first is to skip images in the terminal entirely. A well-designed CLI prints text, and a lot of what terminal-image is used for, a progress indicator, a spinner, a chart, a summary, can be done with text and ANSI colour alone, which is smaller, faster and works everywhere. If the image is decorative, this is the better answer, and it is worth asking whether the image carries information before asking which library to draw it with.

The second is a general-purpose terminal image renderer, rather than this one specifically. The category includes tools that do the same job: a command line program that takes a file path and prints it, and the sibling project terminal-image-cli, which the README lists under Related as the CLI for this module. The difference between them is packaging rather than capability. terminal-image is the library, terminal-image-cli is the program built on it, and choosing the CLI means you inherit its argument handling and its output conventions rather than writing your own around the library.

The third is a different rendering model altogether. Instead of the terminal drawing the image, you can print a link and let the user open it, which for a large or high-resolution image is often better: the terminal shows a filename, and the actual pixels appear in an image viewer. That is the right approach for a CLI that needs to display a chart as part of a report rather than a live preview, and it is what a lot of tools converged on, including the third-party projects the README links in the Related section.

So the deciding question is whether the image must appear inside the terminal. If it must, and you are on a modern terminal, terminal-image gives you the best version of that experience with a four-function API and no dependencies you have to manage. If it does not, a link is smaller and more reliable.

## Conclusion

Adopt terminal-image if you are writing a Node CLI that needs to show an image, and you want it to look right in Kitty, iTerm2 or WezTerm while still working in a plain terminal. Do not adopt it as a general image processing library, since the output is a string of escape codes rather than a file, and do not expect it to work in a terminal with no colour support. Verify first that your terminal is one the protocol detection recognises, particularly if you use tmux or a multiplexer, and that the pixel format of your source images is one the fallback renderer handles well.

## FAQ

### How do I install terminal-image and show an image?

Run `npm install terminal-image`, then import the default export and await the file function, for example `console.log(await terminalImage.file('unicorn.jpg'))`. The package is ESM only and requires node >=22.

### Which terminal graphics protocols does terminal-image support?

It automatically detects and uses the best available: the Kitty Graphics Protocol for full resolution in Kitty, Konsole and WezTerm when configured, the iTerm2 Inline Images Protocol for iTerm2 and compatible terminals, and an ANSI block character fallback using Unicode half-blocks with 24-bit colour.

### How do I control the size of the displayed image?

Pass width and/or height in the options, either as a percentage string such as '50%' or as a number of columns or rows. The image is always scaled to fit the terminal, and the README recommends the percentage option, since a fixed column count overflows a narrow terminal and is then scaled down anyway.

### Does terminal-image support animated GIFs?

Yes, through terminalImage.gifBuffer and terminalImage.gifFile, which return a function that stops the animation when called. The GIF options include maximumFrameRate, defaulting to 30 and ignored when using iTerm, plus renderFrame and renderFrame.done hooks for per-frame and end-of-playback handling.

## Sources

- [Issues](https://github.com/sindresorhus/terminal-image/issues)
- [License: MIT](https://github.com/sindresorhus/terminal-image/blob/main/LICENSE)
- [README](https://github.com/sindresorhus/terminal-image/blob/main/README.md)
- [Releases](https://github.com/sindresorhus/terminal-image/releases)
- [sindresorhus/terminal-image on GitHub](https://github.com/sindresorhus/terminal-image)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sindresorhus-terminal-image
