UNPKG

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