Skip to content

Commit

Permalink
Support animated GIFs (#23)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
Richienb and sindresorhus committed Jul 18, 2020
1 parent d970673 commit cc7cce3
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 38 deletions.
4 changes: 4 additions & 0 deletions example-gif.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';
const terminalImage = require('.');

terminalImage.gifFile('fixture.gif');
Binary file added fixture.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
107 changes: 107 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
/// <reference types="node"/>

declare namespace terminalImage {
export type RenderFrame = {
/**
Custom handler which is run when the animation playback is stopped.
This can be set to perform a cleanup when playback has finished.
*/
done?: () => void;

/**
Custom handler which is run for each frame of the GIF.
This can be set to change how each frame is shown.
@param text - The frame which should be rendered.
*/
(text: string): void;
};
}

declare const terminalImage: {
/**
Display images in the terminal.
Expand Down Expand Up @@ -77,6 +97,93 @@ declare const terminalImage: {
preserveAspectRatio?: boolean;
}>
) => Promise<string>;

/**
Display GIFs in the terminal.
Optionally, you can specify the height and/or width to scale the image.
That can be either the percentage of the terminal window or number of rows and/or columns.
Please note that the image will always be scaled to fit the size of the terminal.
If width and height are not defined, by default the image will take the width and height of the terminal.
It is recommended to use the percentage option.
You can set width and/or height as columns and/or rows of the terminal window as well.
By default, aspect ratio is always maintained. If you don't want to maintain aspect ratio, set preserveAspectRatio to false.
Each frame of the GIF is by default printed to the terminal, overwriting the previous one. To change this behavior, set `renderFrame` to a different function. To change the code run when the animation playback is stopped, set `renderFrame.done` to a different function.
@param imageBuffer - Buffer with the image.
@param options - Image rendering options.
@param options.width - Optional: Custom image width. Can be set as percentage or number of columns of the terminal. It is recommended to use the percentage options.
@param options.height - Optional: Custom image height. Can be set as percentage or number of rows of the terminal. It is recommended to use the percentage options.
@param options.maximumFrameRate - Optional: Maximum framerate to render the GIF. This option is ignored when using iTerm. Defaults to 30.
@param options.renderFrame - Optional: Custom handler which is run for each frame of the GIF.
@param options.renderFrame.done - Optional: Custom handler which is run when the animation playback is stopped.
@returns A function that can be called to stop the GIF animation.
@example
```
import terminalImage = require('terminal-image');
import delay = require('delay');
const {promises: fs} = require('fs');
(async () => {
const gifData = await fs.readFile('unicorn.gif');
const stopAnimation = terminalImage.gifBuffer(gifData);
await delay(5000);
stopAnimation();
})();
```
*/
gifBuffer: (imageBuffer: Readonly<Buffer>, options?: Readonly<{
width?: number;
height?: number;
maximumFrameRate?: number;
renderFrame?: terminalImage.RenderFrame;
}>) => () => void;

/**
Display gifs in the terminal.
Optionally, you can specify the height and/or width to scale the image.
That can be either the percentage of the terminal window or number of rows and/or columns.
Please note that the image will always be scaled to fit the size of the terminal.
If width and height are not defined, by default the image will take the width and height of the terminal.
It is recommended to use the percentage option.
You can set width and/or height as columns and/or rows of the terminal window as well.
By default, aspect ratio is always maintained. If you don't want to maintain aspect ratio, set preserveAspectRatio to false.
Each frame of the gif is by default logged to the terminal, overwriting the previous one. To change this behaviour, set renderFrame to a different function. To change the code run when the animation playback is stopped, set renderFrame.done to a different function.
@param imageBuffer - Buffer with the image.
@param options - Image rendering options.
@param options.width - Optional: Custom image width. Can be set as percentage or number of columns of the terminal. It is recommended to use the percentage options.
@param options.height - Optional: Custom image height. Can be set as percentage or number of rows of the terminal. It is recommended to use the percentage options.
@param options.maximumFrameRate - Optional: Maximum framerate to render the GIF. This option is ignored by iTerm. Defaults to 30.
@param options.renderFrame - Optional: Custom handler which is run for each frame of the gif.
@param options.renderFrame.done - Optional: Custom handler which is run when the animation playback is stopped.
@returns A function that can be called to stop the gif animation.
@example
```
import terminalImage = require('terminal-image');
import delay = require('delay');
(async () => {
const stopAnimation = terminalImage.gifFile('unicorn.gif');
await delay(5000);
stopAnimation();
})();
```
*/
gifFile: (
filePath: string,
options?: Readonly<{
width?: number;
height?: number;
maximumFrameRate?: number;
renderFrame?: terminalImage.RenderFrame;
}>
) => () => void;
};

export = terminalImage;
48 changes: 45 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
'use strict';
const util = require('util');
const {promisify} = require('util');
const fs = require('fs');
const chalk = require('chalk');
const Jimp = require('jimp');
const termImg = require('term-img');
const renderGif = require('render-gif');
const logUpdate = require('log-update');

// `log-update` adds an extra newline so the generated frames need to be 2 pixels shorter.
const ROW_OFFSET = 2;

const PIXEL = '\u2584';
const readFile = util.promisify(fs.readFile);
const readFile = promisify(fs.readFile);

function scale(width, height, originalWidth, originalHeight) {
const originalRatio = originalWidth / originalHeight;
Expand All @@ -33,7 +38,7 @@ function checkAndGetDimensionValue(value, percentageBase) {

function calculateWidthHeight(imageWidth, imageHeight, inputWidth, inputHeight, preserveAspectRatio) {
const terminalColumns = process.stdout.columns || 80;
const terminalRows = process.stdout.rows || 24;
const terminalRows = process.stdout.rows - ROW_OFFSET || 24;

let width;
let height;
Expand Down Expand Up @@ -102,3 +107,40 @@ exports.buffer = async (buffer, {width = '100%', height = '100%', preserveAspect

exports.file = async (filePath, options = {}) =>
exports.buffer(await readFile(filePath), options);

exports.gifBuffer = (buffer, options = {}) => {
options = {
renderFrame: logUpdate,
maximumFrameRate: 30,
...options
};

const finalize = () => {
if (options.renderFrame.done) {
options.renderFrame.done();
}
};

const result = termImg(buffer, {
width: options.width,
height: options.height,
fallback: () => false
});

if (result) {
options.renderFrame(result);
return finalize;
}

const animation = renderGif(buffer, async frameData => {
options.renderFrame(await exports.buffer(Buffer.from(frameData), options));
}, options);

return () => {
animation.isPlaying = false;
finalize();
};
};

exports.gifFile = (filePath, options = {}) =>
exports.gifBuffer(fs.readFileSync(filePath), options);
2 changes: 2 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ import terminalImage = require('.');

expectType<Promise<string>>(terminalImage.file('unicorn.jpg'));
expectType<Promise<string>>(terminalImage.buffer(Buffer.alloc(1)));
expectType<() => void>(terminalImage.gifFile('unicorn.gif'));
expectType<() => void>(terminalImage.gifBuffer(Buffer.alloc(1)));
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@
"jpeg",
"display",
"show",
"pixels"
"pixels",
"gif",
"animation",
"sequence"
],
"dependencies": {
"chalk": "^4.0.0",
"jimp": "^0.14.0",
"log-update": "^4.0.0",
"render-gif": "^2.0.4",
"term-img": "^5.0.0"
},
"devDependencies": {
Expand Down
59 changes: 25 additions & 34 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,81 +58,72 @@ const terminalImage = require('terminal-image');

## API

Supports PNG and JPEG images.
Supports PNG and JPEG images. Animated GIFs are also supported with `.gifBuffer` and `.gifFile`.

### terminalImage.buffer(imageBuffer, options?)
### terminalImage.file(filePath, options?)
### terminalImage.gifBuffer(imageBuffer, options?)
### terminalImage.gifFile(filePath, options?)

Returns a `Promise<string>` with the ansi escape codes to display the image.

##### options
#### options

Type: `object`

###### height
##### height

Type: `string | number`

Custom image height.

Can be set as percentage or number of rows of the terminal. It is recommended to use the percentage options.

###### width
##### width

Type: `string | number`

Custom image width.

Can be set as percentage or number of columns of the terminal. It is recommended to use the percentage options.

###### preserveAspectRatio
##### preserveAspectRatio

Type: `boolean`\
Default: `true`

Whether to maintain image aspect ratio or not.

#### imageBuffer

Type: `Buffer`

Buffer with the image.

### terminalImage.file(filePath, options?)

Returns a `Promise<string>` with the ansi escape codes to display the image.

#### filePath

Type: `string`

File path to the image.
##### maximumFrameRate

##### options
**Only works for `terminalImage.gifBuffer` or `terminalImage.gifFile`**

Type: `object`
Type: `number`\
Default: `30`

###### height
Maximum framerate to render the GIF. This option is ignored when using iTerm.

Type: `string | number`
##### renderFrame

Custom image height.
**Only works for `terminalImage.gifBuffer` or `terminalImage.gifFile`**

Can be set as percentage or number of rows of the terminal. It is recommended to use the percentage options.
Type: `(text: string) => void`\
Default: [log-update](https://github.com/sindresorhus/log-update)

###### width
Custom handler which is run for each frame of the GIF.

Type: `string | number`
This can be set to change how each frame is shown.

Custom image width.
##### renderFrame.done

Can be set as percentage or number of columns of the terminal. It is recommended to use the percentage options.
**Only works for `terminalImage.gifBuffer` or `terminalImage.gifFile`**

###### preserveAspectRatio
Type: `() => void`\
Default: [log-update](https://github.com/sindresorhus/log-update)

Type: `boolean`\
Default: `true`
Custom handler which is run when the animation playback is stopped.

Whether to maintain image aspect ratio or not.
This can be set to perform a cleanup when playback has finished.

## Tip

Expand Down
25 changes: 25 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from 'fs';
import delay from 'delay';
import test from 'ava';
import terminalImage from '.';

Expand All @@ -11,3 +12,27 @@ test('.file()', async t => {
const result = await terminalImage.file('fixture.jpg');
t.is(typeof result, 'string');
});

test('.gifBuffer()', async t => {
let result = '';
const stopAnimation = terminalImage.gifBuffer(fs.readFileSync('fixture.gif'), {
renderFrame: text => {
result += text;
}
});
await delay(500);
stopAnimation();
t.is(typeof result, 'string');
});

test('.gifFile()', async t => {
let result = '';
const stopAnimation = terminalImage.gifFile('fixture.gif', {
renderFrame: text => {
result += text;
}
});
await delay(500);
stopAnimation();
t.is(typeof result, 'string');
});

0 comments on commit cc7cce3

Please sign in to comment.