From f25b7868d74d856ddd107747c99a3dd0d2905fa7 Mon Sep 17 00:00:00 2001 From: Darius Rapalis Date: Mon, 14 Feb 2022 01:11:09 +0200 Subject: [PATCH] automated converting between gif and mp4 with single image convertion separated --- .gitignore | 1 + addAudio.js | 9 ++ convertToAscii.js | 18 +-- extractAudio.js | 9 ++ extractFrames.js | 14 ++ gifToMp4.js | 9 ++ index.js | 322 ++++++++++++++++++++++++++++++++++++----- makeVideoFromImages.js | 9 ++ mp4ToGif.js | 9 ++ package.json | 7 +- readme.md | 55 +++++++ scaleVideo.js | 9 ++ 12 files changed, 423 insertions(+), 48 deletions(-) create mode 100644 addAudio.js create mode 100644 extractAudio.js create mode 100644 extractFrames.js create mode 100644 gifToMp4.js create mode 100644 makeVideoFromImages.js create mode 100644 mp4ToGif.js create mode 100644 readme.md create mode 100644 scaleVideo.js diff --git a/.gitignore b/.gitignore index c08673e..7eb40b0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.png *.mp3 *.gif +*.webm converted-ss/ tmp-ss/ node_modules/ diff --git a/addAudio.js b/addAudio.js new file mode 100644 index 0000000..156143d --- /dev/null +++ b/addAudio.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function addAudio(inputVideoFile = __dirname + '/input.mp4', inputAudioFile = __dirname + '/input.mp3', outputFile = __dirname + '/output.mp4', callback) { + console.log('adding audio to video file'); + exec(`ffmpeg -i ${inputVideoFile} -i ${inputAudioFile} -c:v copy -c:a aac ${outputFile} -y`, () => { + callback(outputFile); + }); + +} diff --git a/convertToAscii.js b/convertToAscii.js index 1add1a3..8e63e4b 100644 --- a/convertToAscii.js +++ b/convertToAscii.js @@ -1,4 +1,5 @@ -module.exports = convertToAscii = async (file, dotSize = 24, verticalSpacinModifier = 4, output = __dirname + '/tmp.img.png') => { +module.exports = convertToAscii = async (file = __dirname + 'input.png', output = __dirname + '/output.png', dotSize = 24, verticalSpacinModifier = 4, callback) => { + console.log('converting image to ascii'); const hobbitImageFile = file; const density = 'Ñ@#W$9876543210?!abc;:+=-,._ '; @@ -33,6 +34,11 @@ module.exports = convertToAscii = async (file, dotSize = 24, verticalSpacinModif ctx.fillStyle = 'black'; ctx.fillRect(0, 0, hobbit.width * dotSize, hobbit.height * dotSize); + + ctx.font = (dotSize + verticalSpacinModifier) + "px Courier"; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = `white`; for(let x = 0; x < hobbit.width; x++) { for(let y = 0; y < hobbit.height; y++) { @@ -42,22 +48,16 @@ module.exports = convertToAscii = async (file, dotSize = 24, verticalSpacinModif const b = hobbit.pixels[pixelPosition + 2]; const a = hobbit.pixels[pixelPosition + 3]; - const avg = Math.floor((r+g+b)/ 3); + const avg = Math.floor((r+g+b)/ 3) * (a/255); const gray = map(255 - avg, 0, 255, 0, density.length); - - ctx.font = (dotSize + verticalSpacinModifier) + "px Courier"; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = `white`; ctx.fillText(density.charAt(gray), x*dotSize + dotSize/2, y*dotSize + dotSize/2); } } ImageDataURI.outputFile(canvas.toDataURL(), output); - console.log(output, 'converted'); res(1); }); - }); + callback(output); } diff --git a/extractAudio.js b/extractAudio.js new file mode 100644 index 0000000..bf4dc5d --- /dev/null +++ b/extractAudio.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function extractAudio(inputFile = __dirname + '/input.gif', outputFile = __dirname + '/output.mp4', callback) { + console.log('extracting audio'); + exec(`ffmpeg -i ${inputFile} ${outputFile} -y`, () => { + callback(outputFile); + }); + +} diff --git a/extractFrames.js b/extractFrames.js new file mode 100644 index 0000000..92d00da --- /dev/null +++ b/extractFrames.js @@ -0,0 +1,14 @@ +const fs = require('fs'); +module.exports = async function extractFrames(inputFile = __dirname + '/input.mp4', outputFolder = __dirname + '/tmp-ss', callback) { + console.log('extracting frames'); + fs.mkdirSync(outputFolder); + + const extractFrames = require('ffmpeg-extract-frames') + + await extractFrames({ + input: inputFile, + output: outputFolder + '/%d.png', + }); + + callback(); +} diff --git a/gifToMp4.js b/gifToMp4.js new file mode 100644 index 0000000..370b6d0 --- /dev/null +++ b/gifToMp4.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function gifToMp4(inputFile = __dirname + '/input.gif', outputFile = __dirname + '/output.gif', callback) { + console.log('converting gif to mp4'); + exec(`ffmpeg -i ${inputFile} ${outputFile} -y`, () => { + callback(outputFile); + }); + +} diff --git a/index.js b/index.js index b508036..d678c01 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,4 @@ /* - PREREQUISITES: - ffmpeg installed and added to the PATH @@ -16,55 +15,302 @@ COMMANDS TO BUILD VIDEO AFTER CONVERSION: - convert video to gif (no audio) ffmpeg -i .\converted-video.mp4 -f gif .\converted-video.gif - */ -const extractFrames = require('ffmpeg-extract-frames') -const convertToAscii = require('./convertToAscii'); +const convertToAscii = require("./convertToAscii"); +const extractFrames = require("./extractFrames"); +const gifToMp4 = require("./gifToMp4"); +const scaleVideo = require("./scaleVideo"); const fs = require('fs'); -const { exec } = require("child_process"); -const fsExtra = require('fs-extra') +const makeVideoFromImages = require("./makeVideoFromImages"); +const mp4ToGif = require("./mp4ToGif"); +const fsExtra = require('fs-extra'); -fsExtra.emptyDirSync(__dirname + '/converted-ss') -fsExtra.emptyDirSync(__dirname + '/tmp-ss') +args = process.argv; -if (!fs.existsSync(__dirname + '/converted-ss')) { - fs.mkdirSync(__dirname + '/converted-ss'); -} -if (!fs.existsSync(__dirname + '/tmp-ss')) { - fs.mkdirSync(__dirname + '/tmp-ss'); -} +args.shift(); +args.shift(); -const PATH_TO_VIDEO_TO_BE_CONVERTED = __dirname + '/sponge-bob-mrs-puff-scaled.mp4'; +const base = 10; -async function run() { - await extractFrames({ - input: PATH_TO_VIDEO_TO_BE_CONVERTED, - output: __dirname + '/tmp-ss/%d.png' - }); +const randomName = (Math.random() + 1).toString(36).substring(2); - let files = fs.readdirSync(__dirname + '/tmp-ss'); +const action = args.shift(); - files = files.filter(name => name.endsWith('.png')); +let usage = ''; +let example = ''; - for (let file of files) { - await convertToAscii(__dirname + '/tmp-ss/' + file, 12, 0, __dirname + '/converted-ss/' + file); - fs.rmSync(__dirname + '/tmp-ss/' + file); - } - exec('ffmpeg -framerate 10 -i ./converted-ss/%d.png -c:v libx264 -r 30 -pix_fmt yuv420p output.mp4 -y', (error, stdout, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - return; +let input; +let output; +let pixelSize; +let pixelModifyer; +let scaledSize; +let fps; + + + +const _scaleVideo = (inputFile, pixelSize, pixelModifyer, scaledSize, fps, output, convertToGif = false, deleteInputFile = false) => { + scaleVideo(inputFile, 'mp4-scaled-' + randomName + '.mp4', scaledSize, fps, (scaledFile) => { + if (deleteInputFile) { + fs.rmSync(inputFile); } - if (stderr) { - console.log(`stderr: ${stderr}`); - return; - } - console.log(`stdout: ${stdout}`); + const outputFolder = __dirname + '/extracted-frames-'+randomName; + extractFrames(scaledFile, outputFolder, async () => { + fs.rmSync(scaledFile); + let files = fs.readdirSync(outputFolder); + const convertedFolder = __dirname + '/converted-frames-'+randomName; + let index = 0; + for (let file of files) { + index++; + await convertToAscii(outputFolder + '/' + file, convertedFolder + '/' + file, pixelSize, pixelModifyer, () => { + console.log(index, '/', files.length); + fs.rmSync(outputFolder + '/' + file); + }); + } + fsExtra.emptyDirSync(outputFolder); + fs.rmdirSync(outputFolder); + makeVideoFromImages(convertedFolder + '/%d.png', __dirname + 'generated-video-' + randomName + '.mp4', fps, fps, (generatedVideoFIle) => { + fsExtra.emptyDirSync(convertedFolder); + fs.rmdirSync(convertedFolder); + if (convertToGif) { + mp4ToGif(generatedVideoFIle, output, (gif) => { + console.log('gif generated in: ' + gif); + }); + } else { + fs.renameSync(generatedVideoFIle, output); + console.log('video generated in: ' + output); + } + }); + }) }); - - console.log('done'); } -run(); +switch (action) { + case 'img': + + usage = `Usage: npm run convert:img input-image-path.png[optional, default:./input.png] output-image-path.png[optional, default:./output.png] pixel-size[optional, default:24] font-size-modifier[optional, degault:4]`; + example = `Example: npm run convert:img input.png output.png 24 0`; + + if (args[0] === 'help') { + console.log(`${usage}\n${example}`); + process.exit(0); + } + + if (args.length > 4) { + console.error(`Invalid parameter count!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[2] && isNaN(parseInt(args[2], base))) { + console.error(`Pixel size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[3] && isNaN(parseInt(args[3], base))) { + console.error(`Font size modifier must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + convertToAscii(args[0], args[1], args[2], args[3], () => { + console.log(`Image converted and saved in ${args[1]}`); + }); + + break; + case 'gif:gif': + + usage = `Usage: npm run convert:gif:gif input-image-path.png[optional, default:./input.gif] output-image-path.png[optional, default:./output.gif] pixel-size[optional, default:24] font-size-modifier[optional, degault:4] scaled-size[optional, default=120] fps[optional, default=10]`; + example = `Example: npm run convert:gif:gif input.gif output.gif 24 4 120 10`; + + if (args[0] === 'help') { + console.log(`${usage}\n${example}`); + process.exit(0); + } + + if (args.length > 7) { + console.error(`Invalid parameter count!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[2] && isNaN(parseInt(args[2], base))) { + console.error(`Pixel size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[3] && isNaN(parseInt(args[3], base))) { + console.error(`Font size modifier must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[4] && isNaN(parseInt(args[4], base))) { + console.error(`Scaled size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[5] && isNaN(parseInt(args[5], base))) { + console.error(`Original FPS must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + input = args[0]; + output = args[1]; + pixelSize = parseInt(args[2], base); + pixelModifyer = parseInt(args[3], base); + scaledSize = parseInt(args[4], base); + fps = parseInt(args[5], base); + + + gifToMp4(input, 'gif-to-mp4-'+randomName+'.mp4', (convertedFile) => { + _scaleVideo(convertedFile, pixelSize, pixelModifyer, scaledSize, fps, output, true, true); + }); + + break; + case 'gif:mp4': + + usage = `Usage: npm run convert:gif:mp4 input-image-path.gif[optional, default:./input.gif] output-image-path.mp4[optional, default:./output.mp4] pixel-size[optional, default:24] font-size-modifier[optional, degault:4] scaled-size[optional, default=120] fps[optional, default=10]`; + example = `Example: npm run convert:gif:mp4 input.gif output.mp4 24 4 120 10`; + + if (args[0] === 'help') { + console.log(`${usage}\n${example}`); + process.exit(0); + } + + if (args.length > 7) { + console.error(`Invalid parameter count!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[2] && isNaN(parseInt(args[2], base))) { + console.error(`Pixel size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[3] && isNaN(parseInt(args[3], base))) { + console.error(`Font size modifier must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[4] && isNaN(parseInt(args[4], base))) { + console.error(`Scaled size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[5] && isNaN(parseInt(args[5], base))) { + console.error(`Original FPS must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + input = args[0]; + output = args[1]; + pixelSize = parseInt(args[2], base); + pixelModifyer = parseInt(args[3], base); + scaledSize = parseInt(args[4], base); + fps = parseInt(args[5], base); + + + gifToMp4(input, 'gif-to-mp4-'+randomName+'.mp4', (convertedFile) => { + _scaleVideo(convertedFile, pixelSize, pixelModifyer, scaledSize, fps, output, false, true); + }); + + break; + case 'mp4:gif': + + usage = `Usage: npm run convert:mp4:gif input-image-path.mp4[optional, default:./input.mp4] output-image-path.gif[optional, default:./output.gif] pixel-size[optional, default:24] font-size-modifier[optional, degault:4] scaled-size[optional, default=120] fps[optional, default=10]`; + example = `Example: npm run convert:mp4:gif input.mp4 output.gif 24 4 120 10`; + + if (args[0] === 'help') { + console.log(`${usage}\n${example}`); + process.exit(0); + } + + if (args.length > 7) { + console.error(`Invalid parameter count!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[2] && isNaN(parseInt(args[2], base))) { + console.error(`Pixel size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[3] && isNaN(parseInt(args[3], base))) { + console.error(`Font size modifier must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[4] && isNaN(parseInt(args[4], base))) { + console.error(`Scaled size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[5] && isNaN(parseInt(args[5], base))) { + console.error(`Original FPS must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + input = args[0]; + output = args[1]; + pixelSize = parseInt(args[2], base); + pixelModifyer = parseInt(args[3], base); + scaledSize = parseInt(args[4], base); + fps = parseInt(args[5], base); + + + _scaleVideo(input, pixelSize, pixelModifyer, scaledSize, fps, output, true, false); + + break; + case 'mp4:mp4': + + usage = `Usage: npm run convert:mp4:mp4 input-image-path.mp4[optional, default:./input.mp4] output-image-path.mp4[optional, default:./output.mp4] pixel-size[optional, default:24] font-size-modifier[optional, degault:4] scaled-size[optional, default=120] fps[optional, default=10]`; + example = `Example: npm run convert:mp4:mp4 input.mp4 output.mp4 24 4 120 10`; + + if (args[0] === 'help') { + console.log(`${usage}\n${example}`); + process.exit(0); + } + + if (args.length > 7) { + console.error(`Invalid parameter count!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[2] && isNaN(parseInt(args[2], base))) { + console.error(`Pixel size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[3] && isNaN(parseInt(args[3], base))) { + console.error(`Font size modifier must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[4] && isNaN(parseInt(args[4], base))) { + console.error(`Scaled size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[5] && isNaN(parseInt(args[5], base))) { + console.error(`Original FPS must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + input = args[0]; + output = args[1]; + pixelSize = parseInt(args[2], base); + pixelModifyer = parseInt(args[3], base); + scaledSize = parseInt(args[4], base); + fps = parseInt(args[5], base); + + + _scaleVideo(input, pixelSize, pixelModifyer, scaledSize, fps, output, false, false); + + break; + + default: +console.log(`Available commands: +convert:gif:gif - converts regular gif to ascii gif +convert:gif:mp4 - converts regular gif to ascii mp4 +convert:mp4:mp4 - converts regular mp4 to ascii mp4 +convert:mp4:gif - converts regular mp4 to ascii gif +convert:img - converts regular image to ascii image\n`); +} diff --git a/makeVideoFromImages.js b/makeVideoFromImages.js new file mode 100644 index 0000000..e763945 --- /dev/null +++ b/makeVideoFromImages.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function makeVideoFromImages(inputFilesPattern = './converted-ss/%d.png', outputFile = __dirname + '/output.mp4', originalFramerate = 10, outputFramerate = 10, callback) { + console.log('combining images to video'); + exec(`ffmpeg -framerate ${originalFramerate} -i ${inputFilesPattern} -c:v libx264 -r ${outputFramerate} -pix_fmt yuv420p ${outputFile} -y`, () => { + callback(outputFile); + }); + +} diff --git a/mp4ToGif.js b/mp4ToGif.js new file mode 100644 index 0000000..381a496 --- /dev/null +++ b/mp4ToGif.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function mp4ToGif(inputFile = __dirname + '/input.gif', outputFile = __dirname + '/output.gif', callback) { + console.log('converting mp4 to gif'); + exec(`ffmpeg -i ${inputFile} -f gif ${outputFile} -y`, () => { + callback(outputFile); + }); + +} diff --git a/package.json b/package.json index 48803c5..e647e90 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,12 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "convert:gif:gif": "node index gif:gif", + "convert:gif:mp4": "node index gif:mp4", + "convert:mp4:mp4": "node index mp4:mp4", + "convert:mp4:gif": "node index mp4:gif", + "convert:img": "node index img", + "help": "node index" }, "author": "", "license": "ISC", diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ae29d17 --- /dev/null +++ b/readme.md @@ -0,0 +1,55 @@ +# ASCII Video Converter + +## Prerequisites + +``` + - nodejs: https://nodejs.org/en/download/ + - ffmpeg: https://www.ffmpeg.org/download.html +``` + +## Commands + +### Options + - input-file + - file to be converted (original is kept) + - default: input.mp4/input.gif/input.png depending on command used + + - output-file + - file to save the converted file + - default: input.mp4/input.gif/input.png depending on command used + + - pixel-size + - size of ASCII character (output resolution will be multipied by this amount, downscaling is recomended) + - default: 24 + + - font-size-modifier + - increase ASCII character size by set pixes without increasing space it takes. Could be useful when using bigger pixel-size setting + - default: 4 + + - scaled-size + - pixels in width (result will be scaled-size * pixel-size of width) + - default: 120 + + - fps + - frames per second of generated file + - default: 10 + +### Convert GIF to ASCII GIF + +`npm run convert:gif:gif [input-file] [output-file] [pixel-size] [font-size-modifier] [scaled-size] [fps]` + +### Convert GIF to ASCII MP4 + +`npm run convert:gif:mp4 [input-file] [output-file] [pixel-size] [font-size-modifier] [scaled-size] [fps]` + +### Convert MP4 to ASCII MP4 + +`npm run convert:mp4:mp4 [input-file] [output-file] [pixel-size] [font-size-modifier] [scaled-size] [fps]` + +### Convert MP4 to ASCII GIF + +`npm run convert:mp4:gif [input-file] [output-file] [pixel-size] [font-size-modifier] [scaled-size] [fps]` + +### Convert PNG to ASCII PNG + +`npm run convert:img [input-gif-file] [output-gif-file] [pixel-size] [font-size-modifier]` diff --git a/scaleVideo.js b/scaleVideo.js new file mode 100644 index 0000000..ad495c7 --- /dev/null +++ b/scaleVideo.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function scaleVideo(inputFile = __dirname + '/input.mp4', outputFile = __dirname + '/output.mp4', height = 120, fps = 10, callback) { + console.log('scaling video'); + exec(`ffmpeg.exe -i ${inputFile} -vf "scale=${height}:-2, fps=${fps} " ${outputFile} -y`, () => { + callback(outputFile); + }); + +}