UNPKG

4.11 kBJavaScriptView Raw
1import fs, {promises as fsPromises} from 'node:fs';
2import chalk from 'chalk';
3import Jimp from 'jimp';
4import termImg from 'term-img';
5import renderGif from 'render-gif';
6import logUpdate from 'log-update';
7
8// `log-update` adds an extra newline so the generated frames need to be 2 pixels shorter.
9const ROW_OFFSET = 2;
10
11const PIXEL = '\u2584';
12
13function scale(width, height, originalWidth, originalHeight) {
14 const originalRatio = originalWidth / originalHeight;
15 const factor = (width / height > originalRatio ? height / originalHeight : width / originalWidth);
16 width = factor * originalWidth;
17 height = factor * originalHeight;
18 return {width, height};
19}
20
21function checkAndGetDimensionValue(value, percentageBase) {
22 if (typeof value === 'string' && value.endsWith('%')) {
23 const percentageValue = Number.parseFloat(value);
24 if (!Number.isNaN(percentageValue) && percentageValue > 0 && percentageValue <= 100) {
25 return Math.floor(percentageValue / 100 * percentageBase);
26 }
27 }
28
29 if (typeof value === 'number') {
30 return value;
31 }
32
33 throw new Error(`${value} is not a valid dimension value`);
34}
35
36function calculateWidthHeight(imageWidth, imageHeight, inputWidth, inputHeight, preserveAspectRatio) {
37 const terminalColumns = process.stdout.columns || 80;
38 const terminalRows = process.stdout.rows - ROW_OFFSET || 24;
39
40 let width;
41 let height;
42
43 if (inputHeight && inputWidth) {
44 width = checkAndGetDimensionValue(inputWidth, terminalColumns);
45 height = checkAndGetDimensionValue(inputHeight, terminalRows) * 2;
46
47 if (preserveAspectRatio) {
48 ({width, height} = scale(width, height, imageWidth, imageHeight));
49 }
50 } else if (inputWidth) {
51 width = checkAndGetDimensionValue(inputWidth, terminalColumns);
52 height = imageHeight * width / imageWidth;
53 } else if (inputHeight) {
54 height = checkAndGetDimensionValue(inputHeight, terminalRows) * 2;
55 width = imageWidth * height / imageHeight;
56 } else {
57 ({width, height} = scale(terminalColumns, terminalRows * 2, imageWidth, imageHeight));
58 }
59
60 if (width > terminalColumns) {
61 ({width, height} = scale(terminalColumns, terminalRows * 2, width, height));
62 }
63
64 width = Math.round(width);
65 height = Math.round(height);
66
67 return {width, height};
68}
69
70async function render(buffer, {width: inputWidth, height: inputHeight, preserveAspectRatio}) {
71 const image = await Jimp.read(buffer);
72 const {bitmap} = image;
73
74 const {width, height} = calculateWidthHeight(bitmap.width, bitmap.height, inputWidth, inputHeight, preserveAspectRatio);
75
76 image.resize(width, height);
77
78 let result = '';
79 for (let y = 0; y < image.bitmap.height - 1; y += 2) {
80 for (let x = 0; x < image.bitmap.width; x++) {
81 const {r, g, b, a} = Jimp.intToRGBA(image.getPixelColor(x, y));
82 const {r: r2, g: g2, b: b2} = Jimp.intToRGBA(image.getPixelColor(x, y + 1));
83 result += a === 0 ? chalk.reset(' ') : chalk.bgRgb(r, g, b).rgb(r2, g2, b2)(PIXEL);
84 }
85
86 result += '\n';
87 }
88
89 return result;
90}
91
92const terminalImage = {};
93
94terminalImage.buffer = async (buffer, {width = '100%', height = '100%', preserveAspectRatio = true} = {}) => {
95 return termImg(buffer, {
96 width,
97 height,
98 fallback: () => render(buffer, {height, width, preserveAspectRatio})
99 });
100};
101
102terminalImage.file = async (filePath, options = {}) =>
103 terminalImage.buffer(await fsPromises.readFile(filePath), options);
104
105terminalImage.gifBuffer = (buffer, options = {}) => {
106 options = {
107 renderFrame: logUpdate,
108 maximumFrameRate: 30,
109 ...options
110 };
111
112 const finalize = () => {
113 if (options.renderFrame.done) {
114 options.renderFrame.done();
115 }
116 };
117
118 const result = termImg(buffer, {
119 width: options.width,
120 height: options.height,
121 fallback: () => false
122 });
123
124 if (result) {
125 options.renderFrame(result);
126 return finalize;
127 }
128
129 const animation = renderGif(buffer, async frameData => {
130 options.renderFrame(await terminalImage.buffer(Buffer.from(frameData), options));
131 }, options);
132
133 return () => {
134 animation.isPlaying = false;
135 finalize();
136 };
137};
138
139terminalImage.gifFile = (filePath, options = {}) =>
140 terminalImage.gifBuffer(fs.readFileSync(filePath), options);
141
142export default terminalImage;