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 + +
Turn video into sheet of images
+Stitch rendered frames into strips
+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('