UNPKG

@range3/holo

Version:

Hololive Tools for CLI

168 lines (133 loc) 5.06 kB
// MIT License // Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com) // Copyright 2020 range3 (https://github.com/range3) 'use strict' 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') const sixel = require('sixel') // `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 = promisify(fs.readFile) function scale (width, height, originalWidth, originalHeight) { const originalRatio = originalWidth / originalHeight const factor = (width / height > originalRatio ? height / originalHeight : width / originalWidth) width = factor * originalWidth height = factor * originalHeight return { width, height } } function checkAndGetDimensionValue (value, percentageBase) { if (typeof value === 'string' && value.endsWith('%')) { const percentageValue = Number.parseFloat(value) if (!Number.isNaN(percentageValue) && percentageValue > 0 && percentageValue <= 100) { return Math.floor(percentageValue / 100 * percentageBase) } } if (typeof value === 'number') { return value } throw new Error(`${value} is not a valid dimension value`) } function calculateWidthHeight (imageWidth, imageHeight, inputWidth, inputHeight, preserveAspectRatio) { const terminalColumns = process.stdout.columns || 80 const terminalRows = process.stdout.rows - ROW_OFFSET || 24 let width let height if (inputHeight && inputWidth) { width = checkAndGetDimensionValue(inputWidth, terminalColumns) height = checkAndGetDimensionValue(inputHeight, terminalRows) * 2 if (preserveAspectRatio) { ({ width, height } = scale(width, height, imageWidth, imageHeight)) } } else if (inputWidth) { width = checkAndGetDimensionValue(inputWidth, terminalColumns) height = imageHeight * width / imageWidth } else if (inputHeight) { height = checkAndGetDimensionValue(inputHeight, terminalRows) * 2 width = imageWidth * height / imageHeight } else { ({ width, height } = scale(terminalColumns, terminalRows * 2, imageWidth, imageHeight)) } if (width > terminalColumns) { ({ width, height } = scale(terminalColumns, terminalRows * 2, width, height)) } width = Math.round(width) height = Math.round(height) return { width, height } } async function render (buffer, { width: inputWidth, height: inputHeight, preserveAspectRatio }) { const image = await Jimp.read(buffer) const { bitmap } = image const { width, height } = calculateWidthHeight(bitmap.width, bitmap.height, inputWidth, inputHeight, preserveAspectRatio) image.resize(width, height) let result = '' for (let y = 0; y < image.bitmap.height - 1; y += 2) { for (let x = 0; x < image.bitmap.width; x++) { const { r, g, b, a } = Jimp.intToRGBA(image.getPixelColor(x, y)) const { r: r2, g: g2, b: b2 } = Jimp.intToRGBA(image.getPixelColor(x, y + 1)) if (a === 0) { result += chalk.reset(' ') } else { result += chalk.bgRgb(r, g, b).rgb(r2, g2, b2)(PIXEL) } } result += '\n' } return result } async function renderSixel (buffer, { width: inputWidth, height: inputHeight, preserveAspectRatio }) { const image = await Jimp.read(buffer) const { bitmap } = image const CELL_TO_PIXEL_FACTOR = 7 let { width, height } = calculateWidthHeight(bitmap.width, bitmap.height, inputWidth, inputHeight, preserveAspectRatio) width *= CELL_TO_PIXEL_FACTOR height *= CELL_TO_PIXEL_FACTOR image.resize(width, height) return sixel.image2sixel(Uint8ClampedArray.from(image.bitmap.data), image.bitmap.width, image.bitmap.height) } exports.buffer = async (buffer, { width = '100%', height = '100%', preserveAspectRatio = true, sixel = false } = {}) => { return termImg(buffer, { width, height, fallback: () => sixel ? renderSixel(buffer, { height, width, preserveAspectRatio }) : render(buffer, { height, width, preserveAspectRatio }), }) } 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)