diff --git a/.gitignore b/.gitignore index 7e51146..aa4b8ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ +node_modules tmp/* temp/* +.nexe \ No newline at end of file diff --git a/Readme.md b/Readme.md index e3a5c59..23c196f 100644 --- a/Readme.md +++ b/Readme.md @@ -3,7 +3,7 @@ v2f Convert video to 16mm-sized strips of frames. For transferring to acetate and other experimental needs. -Look how easy it is to use: +Use the distributed binary ./v2f ./path_to_video.mov 300 @@ -22,7 +22,7 @@ Releases Dependencies ------------ -- node.js (or use a compiled version) +- node.js (or use a [released version](https://github.com/sixteenmillimeter/v2f/releases/)) - libav - ImageMagick @@ -37,11 +37,14 @@ Ubuntu apt-get install libav imagemagick + + Contribute ---------- - Issue Tracker: https://github.com/sixteenmillimeter/v2f/issues - Source Code: https://github.com/sixteenmillimeter/v2f +- Home Page: https://sixteenmillimeter.com/projects/v2f Support ------- diff --git a/docs/Readme.md b/docs/Readme.md new file mode 100644 index 0000000..e1e1c82 --- /dev/null +++ b/docs/Readme.md @@ -0,0 +1,36 @@ +## Functions + +
+
convert(path, dpi, length)
+

Turn video into sheet of images

+
+
stitch(loc, dim)
+

Stitch rendered frames into strips

+
+
+ + + +## convert(path, dpi, length) +Turn video into sheet of images + +**Kind**: global function + +| Param | Type | Description | +| --- | --- | --- | +| path | String | file path (absolute) | +| dpi | Integer | target printing dpi | +| length | Integer | strip length in frames | + + + +## stitch(loc, dim) +Stitch rendered frames into strips + +**Kind**: global function + +| Param | Type | Description | +| --- | --- | --- | +| loc | String | Path of folder containing frames | +| dim | Object | Dimensions object | + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6845444 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,26 @@ +{ + "name": "v2f", + "version": "1.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "async": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", + "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "requires": { + "lodash": "4.17.4" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + } + } +} diff --git a/package.json b/package.json index d57d4d2..b080177 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "v2f", - "version": "1.0.0", + "version": "1.1.0", "description": "Turn a video into strips of precise 16mm-size stills", "main": "v2f.js", "scripts": { @@ -8,12 +8,16 @@ }, "author": "mmcwilliams", "license": "MIT", - "nexe" : { - "output" : "../video_to_page_nexe/v2f", - "runtime": { - "ignoreFlags": true, - "framework" : "nodejs", - "version" : "5.0.0" - } + "nexe": { + "output": "../video_to_page_nexe/v2f", + "runtime": { + "ignoreFlags": true, + "framework": "nodejs", + "version": "8.7.0" + } + }, + "dependencies": { + "async": "^2.5.0", + "commander": "^2.11.0" } } diff --git a/v2f.js b/v2f.js index d85a056..0ef9b9c 100755 --- a/v2f.js +++ b/v2f.js @@ -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(' ') + .version('1.1.0') + .option('-i, --input ', 'Video source to print to film strip, anything that avconv can read') + .option('-o, --output ', 'Output directory, will print images on A4 standard paper file') + .option('-d, --dpi ', 'DPI output pages') + .option('-f, --film ', 'Choose film gauge: 16mm, super16, 35mm') + .option('-v, --verbose', 'Run in verbose mode') + .parse(args) + +initialize(cmd) \ No newline at end of file