v2f/v2f.js

210 lines
6.8 KiB
JavaScript

'use strict';
const cmd = require('commander');
const async = require('async');
const exec = require('child_process').exec;
const fs = require('fs');
const path = require('path');
const os = require('os');
const osTmp = os.tmpdir();
const TMP = path.join(osTmp, '/v2f/');
const pkg = require('./package.json');
class Dimensions {
constructor(filmStr, dpi) {
const IN = dpi / 25.4;
const film = this._gauge(filmStr);
this.h = Math.round(film.h * IN); //frame height
this.w = Math.round(film.w * IN); //frame width
this.o = Math.round(film.o * IN); //space between columns
this.dpi = dpi;
this.film = film;
}
_gauge(film) {
if (film === '16mm') {
return {
h: 7.62,
w: 10.5,
o: 16
};
}
else if (film === 'super16') {
return {
h: 7.62,
w: 12.75,
o: 16
};
}
else if (film === '35mm') {
return {
h: 19.05,
w: 22,
o: 35
};
}
else {
error('Film type not found, see --help for more info');
}
}
}
/**
*
*
* @function
* @param {Object} Commander object
*/
function initialize(command) {
const dpi = command.dpi || 300;
const film = command.film || '16mm';
const input = command.input || command.args[0] || error('No input file, see --help for more info');
const output = command.output || command.args[1] || error('No ouput directory, see --help for more info');
const dim = new Dimensions(film, dpi);
const pageW = command.width || 8.5;
const pageL = command.length || 11;
const exe = command.executable || 'avconv';
const negative = typeof command.negative !== 'undefined' ? true : false;
if (!fs.existsSync(input))
error(`Video "${input}" cannot be found`);
async.series([
(next) => {
convert(exe, input, dim, negative, next);
},
(next) => {
stitch(output, dim, next, pageW, pageL);
},
cleanup
], () => {
console.log(`Finished creating pages`);
});
}
/** *
* Create image sequence from source video, using
*
* @function
* @param {String} input file path (absolute)
* @param {Integer} dpi target printing dpi
* @param {Integer} length strip length in frames
*
*/
function convert(exe, input, dim, negative = false, next) {
const file = input.split('/').pop();
const negStr = negative ? `-vf lutrgb="r=negval:g=negval:b=negval"` : '';
const execStr = `${exe} -i "${input}" -s ${dim.w}x${dim.h} -qscale 1 ${negStr} "${TMP}v2f_sequence_%04d.jpg"`;
console.log(`Converting ${file}...`);
console.log(`Exporting all frames with aspect ratio: ${dim.w / dim.h}...`);
if (!fs.existsSync(TMP))
fs.mkdirSync(TMP);
exec(execStr, (ste, std) => {
if (ste) {
return error(ste);
}
console.log('Frames exported successfully!');
next();
});
}
/** *
* Stitch rendered frames into strips
* @function
* @param {String} output Path of folder containing frames
* @param {Object} dim Dimensions object
* @param {Function} next Async lib callback function
* @param {Integer} pageW Page width in inches
* @param {Integer} pageL Page length in inches
*
*/
function stitch(output, dim, next, pageW, pageL) {
const length = Math.floor((pageL * dim.dpi) / dim.h) - 1;
const width = Math.floor((pageW * dim.dpi / dim.o)) - 1;
const loc = TMP.substring(0, TMP.length - 1);
const diff = Math.round((dim.o - dim.w) / 2);
let page = 0;
let pageCount = 0;
let cmd = `find "${loc}" -type f -name "v2f_sequence_*.jpg"`;
console.log('Stitching frames into sheets...');
console.log(`Sheets will contain ${width}x${length} frames...`);
exec(cmd, (ste, std) => {
if (ste) {
return error(ste);
}
let jobs = [];
let cmds = [];
let frames = std.split('\n');
let execStr = 'montage ';
let pagePath = ``;
let i = 0;
frames = frames.filter((elem) => {
if (elem.indexOf('find: ') === -1) {
return elem;
}
});
frames.sort();
for (let frame of frames) {
execStr += `${frame} `;
if ((i + 1) % (width * length) === 0 || i === frames.length - 1) {
pagePath = path.join(output, `./page_${pad(page)}.jpg`);
execStr += `\ -tile 1x${length} -geometry ${dim.w}x${dim.h}+${diff}+0 miff:- |\ \nmontage - -geometry +0+0 -tile ${width}x1 -density ${dim.dpi} "${pagePath}"`;
cmds.push(execStr);
execStr = 'montage ';
page++;
}
i++;
}
jobs = cmds.map((cmd) => {
return (cb) => {
exec(cmd, (err, std, ste) => {
if (err) {
return error(err);
}
console.log(`Created page of ${width}x${length} frames!`);
cb();
});
};
});
async.series(jobs, next);
});
}
function cleanup(next) {
console.log('Cleaning up...');
exec(`rm -r "${TMP}"`, (err) => {
if (err)
console.error(err);
if (next)
next();
});
}
function pad(n) {
return ('00000' + n).slice(-5);
}
var error = function (err) {
if (process.argv.indexOf('-v') !== -1 || process.argv.indexOf('--verbose') !== -1) {
console.error(err);
}
else {
console.error('Error running program. Run in verbose mode for more info (-v,--verbose)');
}
process.exit(1);
};
process.on('uncaughtException', err => {
error(err);
});
//convert(process.argv[2], process.argv[3])
//fix for nexe
let args = [].concat(process.argv);
if (args[1].indexOf('v2f.js') === -1) {
args.reverse();
args.push('node');
args.reverse();
}
cmd.arguments('<input> <output>')
.version(pkg.version)
.option('-i, --input <path>', 'Video source to print to film strip, anything that avconv can read')
.option('-o, --output <path>', 'Output directory, will render images on specified page size')
.option('-d, --dpi <dpi>', 'DPI output pages')
.option('-f, --film <gauge>', 'Choose film gauge: 16mm, super16, 35mm')
.option('-w, --width <inches>', 'Output page width, in inches. Default 8.5')
.option('-l, --length <inches>', 'Output page length, in inches. Default 11')
.option('-e, --executable <binary>', 'Alternate binary to use in place of avconv, ie ffmpeg')
.option('-v, --verbose', 'Run in verbose mode')
.option('-n, --negative', 'Invert color channels to create negative')
.parse(args);
initialize(cmd);