1 | 'use strict';
|
2 | const {promisify} = require('util');
|
3 | const fs = require('fs');
|
4 | const chalk = require('chalk');
|
5 | const Jimp = require('jimp');
|
6 | const termImg = require('term-img');
|
7 | const renderGif = require('render-gif');
|
8 | const logUpdate = require('log-update');
|
9 |
|
10 |
|
11 | const ROW_OFFSET = 2;
|
12 |
|
13 | const PIXEL = '\u2584';
|
14 | const readFile = promisify(fs.readFile);
|
15 |
|
16 | function 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 |
|
24 | function 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 |
|
39 | function 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 |
|
73 | async 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 |
|
100 | exports.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 |
|
108 | exports.file = async (filePath, options = {}) =>
|
109 | exports.buffer(await readFile(filePath), options);
|
110 |
|
111 | exports.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 |
|
145 | exports.gifFile = (filePath, options = {}) =>
|
146 | exports.gifBuffer(fs.readFileSync(filePath), options);
|