|
|
|
@ -1,152 +1,214 @@
|
|
|
|
|
var exec = require('child_process').exec,
|
|
|
|
|
fs = require('fs'),
|
|
|
|
|
_tmp = './temp';
|
|
|
|
|
/*jshint strict: true, esversion:6, node: true, asi: true*/
|
|
|
|
|
|
|
|
|
|
//var frame_height = 7.61;
|
|
|
|
|
//var frame_height = 7.49;
|
|
|
|
|
var frame_height = 7.62;
|
|
|
|
|
var frame_padding = 0;
|
|
|
|
|
'use strict'
|
|
|
|
|
const cmd = require('commander')
|
|
|
|
|
const async = require('async')
|
|
|
|
|
const exec = require('child_process').exec
|
|
|
|
|
const fs = require('fs')
|
|
|
|
|
const path = require('path')
|
|
|
|
|
|
|
|
|
|
var frame_dimensions = function (dpi) {
|
|
|
|
|
var h = Math.round(frame_height * (dpi / 25.4)),
|
|
|
|
|
w = Math.round(10.5 * (dpi / 25.4)),
|
|
|
|
|
o = Math.round(16 * (dpi / 25.4));
|
|
|
|
|
return {h: h, w: w, o: o, dpi: dpi};
|
|
|
|
|
};
|
|
|
|
|
const TMP = '/tmp/v2f/'
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
convert() - Turn video into sheet of images
|
|
|
|
|
class Dimensions{
|
|
|
|
|
constructor (filmStr, dpi) {
|
|
|
|
|
const IN = dpi / 25.4
|
|
|
|
|
const film = this._gauge(filmStr)
|
|
|
|
|
|
|
|
|
|
@param: path - file path (absolute)
|
|
|
|
|
@param: dpi - target printing dpi
|
|
|
|
|
@param: length - strip length in frames
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
var convert = function (path, dpi) {
|
|
|
|
|
'use strict';
|
|
|
|
|
var dim = frame_dimensions(dpi),
|
|
|
|
|
file = path.split('/').pop(),
|
|
|
|
|
loc = _tmp + '/',
|
|
|
|
|
execStr = 'avconv -i "' + path + '" -s ' + dim.w + 'x' + dim.h + ' -qscale 1 "' + loc + 'sequence_%04d.jpg"';
|
|
|
|
|
|
|
|
|
|
console.log('Converting ' + file + '...');
|
|
|
|
|
console.log('Exporting all frames with aspect ratio: ' + (dim.w / dim.h) + '...');
|
|
|
|
|
|
|
|
|
|
fs.mkdirSync(_tmp);
|
|
|
|
|
|
|
|
|
|
exec(execStr, function (ste, std) {
|
|
|
|
|
if (ste) {
|
|
|
|
|
return errorHandle(ste);
|
|
|
|
|
}
|
|
|
|
|
console.log('Frames exported successfully!');
|
|
|
|
|
stitch(loc.substring(0, loc.length - 1), dim);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var stitch = function (loc, dim) {
|
|
|
|
|
'use strict';
|
|
|
|
|
var length = Math.floor((11 * dim.dpi) / dim.h) - 1,
|
|
|
|
|
width = Math.floor((8.5 * dim.dpi / dim.o)) - 1,
|
|
|
|
|
page = 0,
|
|
|
|
|
pageCount = 0,
|
|
|
|
|
cmd = 'find "' + loc + '" -type f -name "sequence_*.jpg"',
|
|
|
|
|
find_cb = function (ste, std) {
|
|
|
|
|
if (ste) {
|
|
|
|
|
return errorHandle(ste);
|
|
|
|
|
}
|
|
|
|
|
var frames = std.split('\n'),
|
|
|
|
|
execStr = 'montage ',
|
|
|
|
|
montage_cb = function (stee, stdd) {
|
|
|
|
|
if (stee) {
|
|
|
|
|
return errorHandle(ste);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('Created page_' + pageCount + '.jpg!');
|
|
|
|
|
pageCount++;
|
|
|
|
|
if (pageCount === page) {
|
|
|
|
|
console.log('Cleaning up...');
|
|
|
|
|
setTimeout(function () {
|
|
|
|
|
exec('find "' + loc + '" -type f -name "sequence_*.jpg" -delete', function () {
|
|
|
|
|
fs.rmdirSync(_tmp);
|
|
|
|
|
console.log('Done!');
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
for (var i = 0; i < frames.length; i++) {
|
|
|
|
|
execStr += frames[i] + ' ';
|
|
|
|
|
if ((i + 1) % (width * length) === 0 || i === frames.length - 1) {
|
|
|
|
|
execStr += '\ -tile 1x' + length + ' -geometry ' + dim.w + 'x' + dim.h + '+' + Math.round((dim.o - dim.w) / 2) + '+0 miff:- |\ \nmontage - -geometry +0+0 -tile ' + width + 'x1 -density ' + dim.dpi + ' "./page_' + page + '.jpg"';
|
|
|
|
|
exec(execStr, montage_cb);
|
|
|
|
|
execStr = 'montage ';
|
|
|
|
|
page++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loc = _tmp;
|
|
|
|
|
|
|
|
|
|
console.log('Stitching frames into sheets...');
|
|
|
|
|
console.log('Sheets will contain ' + width + 'x' + length + ' frames...');
|
|
|
|
|
exec(cmd, find_cb);
|
|
|
|
|
};
|
|
|
|
|
var errorHandle = function (err) {
|
|
|
|
|
'use strict';
|
|
|
|
|
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)');
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
process.exit(1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
process.on('uncaughtException', function (err) {
|
|
|
|
|
errorHandle(err);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof process.argv[2] === 'undefined') {
|
|
|
|
|
console.error('No path to video defined');
|
|
|
|
|
process.exit(1);
|
|
|
|
|
_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')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof process.argv[3] === 'undefined') {
|
|
|
|
|
process.argv[3] = 300;
|
|
|
|
|
console.log('Using default 300dpi');
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* @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)
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(input)) error(`Video "${input}" cannot be found`)
|
|
|
|
|
|
|
|
|
|
async.series([
|
|
|
|
|
next => {
|
|
|
|
|
convert(input, dim, next)
|
|
|
|
|
},
|
|
|
|
|
next => {
|
|
|
|
|
stitch(output, dim, next)
|
|
|
|
|
},
|
|
|
|
|
cleanup
|
|
|
|
|
], () => {
|
|
|
|
|
console.log(`Finished creating pages`)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
convert(process.argv[2], process.argv[3]);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
|
|
INSTALLATION AND RUNNING
|
|
|
|
|
|
|
|
|
|
/** *
|
|
|
|
|
* 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 (input, dim, next) {
|
|
|
|
|
const file = input.split('/').pop()
|
|
|
|
|
const execStr = `avconv -i "${input}" -s ${dim.w}x${dim.h} -qscale 1 "${TMP}v2f_sequence_%04d.jpg"`
|
|
|
|
|
|
|
|
|
|
//Install in this order to satisfy requirements: GCC required by MacPorts, etc.
|
|
|
|
|
console.log(`Converting ${file}...`)
|
|
|
|
|
console.log(`Exporting all frames with aspect ratio: ${dim.w / dim.h}...`)
|
|
|
|
|
|
|
|
|
|
//Install Node http://nodejs.org/dist/v0.10.22/node-v0.10.22.pkg
|
|
|
|
|
//Install GCC https://github.com/kennethreitz/osx-gcc-installer#option-1-downloading-pre-built-binaries
|
|
|
|
|
//Install MacPorts http://www.macports.org/install.php
|
|
|
|
|
//Install ImageMagick in terminal type "sudo port install ImageMagick"
|
|
|
|
|
//Install avconv -see below
|
|
|
|
|
if (!fs.existsSync(TMP)) fs.mkdirSync(TMP)
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
Download and unzip http://libav.org/releases/libav-9.6.tar.gz
|
|
|
|
|
in terminal type "cd " and drag in unzipped folder and hit enter
|
|
|
|
|
in terminal copy "sudo port install yasm zlib bzip2 faac lame speex libogg libvorbis libtheora libvpx x264 XviD openjpeg15 opencore-amr freetype" (without quotes) hit enter
|
|
|
|
|
in terminal copy "./configure \ --enable-gpl --enable-libx264 --enable-libxvid \ --enable-version3 --enable-libopencore-amrnb --enable-libopencore-amrwb \ --enable-nonfree --enable-libfaac \ --enable-libmp3lame --enable-libspeex --enable-libvorbis --enable-libtheora --enable-libvpx \ --enable-libopenjpeg --enable-libfreetype --enable-doc --enable-gnutls --enable-shared" hit enter
|
|
|
|
|
(if that gives errors just use "./configure" and hit enter)
|
|
|
|
|
in terminal copy "make && sudo make install" hit enter
|
|
|
|
|
exec(execStr, (ste, std) => {
|
|
|
|
|
if (ste) {
|
|
|
|
|
return error(ste)
|
|
|
|
|
}
|
|
|
|
|
console.log('Frames exported successfully!')
|
|
|
|
|
next()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
that will install it
|
|
|
|
|
/** *
|
|
|
|
|
* 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
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
function stitch (output, dim, next) {
|
|
|
|
|
const length = Math.floor((11 * dim.dpi) / dim.h) - 1
|
|
|
|
|
const width = Math.floor((8.5 * 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"`
|
|
|
|
|
|
|
|
|
|
//run by going to terminal typing "node " dragging this script into the terminal, dragging the video into the terminal and adding a DPI value and hitting enter
|
|
|
|
|
//the command should look something like this:
|
|
|
|
|
//node /Users/stenzel/Desktop/video_to_page.js /Users/stenzel/Desktop/PaulRobeson/Paul\ Robeson\ discusses\ Othello.mp4 600
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
//Get the absolute path of any file by dragging it into terminal and copying the results
|
|
|
|
|
//Must be enclosed by ''
|
|
|
|
|
//Will generate pages and frames in same folder as source video
|
|
|
|
|
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}"`
|
|
|
|
|
console.log(execStr)
|
|
|
|
|
process.exit()
|
|
|
|
|
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('1.1.0')
|
|
|
|
|
.option('-i, --input <path>', 'Video source to print to film strip, anything that avconv can read')
|
|
|
|
|
.option('-o, --output <path>', 'Output directory, will print images on A4 standard paper file')
|
|
|
|
|
.option('-d, --dpi <dpi>', 'DPI output pages')
|
|
|
|
|
.option('-f, --film <gauge>', 'Choose film gauge: 16mm, super16, 35mm')
|
|
|
|
|
.option('-v, --verbose', 'Run in verbose mode')
|
|
|
|
|
.parse(args)
|
|
|
|
|
|
|
|
|
|
initialize(cmd)
|