Skip to content

Commit

Permalink
Refactor: split out code into files + separate image/video processing
Browse files Browse the repository at this point in the history
  • Loading branch information
rprieto committed Jan 1, 2019
1 parent 1b5e1ba commit 0b881dd
Show file tree
Hide file tree
Showing 13 changed files with 129 additions and 102 deletions.
18 changes: 0 additions & 18 deletions lib/gmargs.js

This file was deleted.

File renamed without changes.
66 changes: 66 additions & 0 deletions lib/image/gmagick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const gm = require('gm')

const DEFAULT_PHOTO_QUALITY = 90 // percent

exports.prepare = function (source, options) {
// start processing with GraphicsMagick
const image = gm(source)

// read baked-in orientation info, and output a rotated image with orientation=0
image.autoOrient()

// optional watermark
const cropping = options.width && options.height
if (options.watermark && !cropping) {
image.composite(options.watermark.file)
if (options.watermark.position === 'Repeat') {
image.tile(options.watermark.file)
} else if (typeof options.watermark.position === 'string') {
image.gravity(options.watermark.position)
} else {
image.gravity('SouthEast')
}
}

// resize if necessary
if (cropping) {
// crop to the exact height and weight
image.resize(options.width, options.height, '^')
image.gravity('Center')
image.crop(options.width, options.height)
image.out('+repage')
} else if (options.height) {
// resize to a maximum height
image.resize(null, options.height, '>')
} else if (options.width) {
// resize to a maximum width
image.resize(options.width, null, '>')
}

// default quality, for typical web-friendly sizes
image.quality(options.quality || DEFAULT_PHOTO_QUALITY)

// apply custom post-processing arguments (sharpen, brightness...)
exports.addRawArgs(image, options.args)

return image
}

/*
Applies an array of GraphicsMagick string arguments to a <gm> instance
e.g. apply(image, ['--modulate 120'])
*/
exports.addRawArgs = function (image, args) {
if (args && args.length) {
args.forEach(arg => {
const index = arg.indexOf(' ')
if (index === -1) {
image.out(arg)
} else {
const command = arg.substr(0, index)
const values = arg.substr(index + 1)
image.out(command, values)
}
})
}
}
81 changes: 8 additions & 73 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
const async = require('async')
const fs = require('fs')
const gm = require('gm')
const mkdirp = require('mkdirp')
const path = require('path')
const gifsicle = require('./gifsicle')
const gmargs = require('./gmargs')
const ffmpeg = require('./ffmpeg')
const gifsicle = require('./image/gifsicle')
const gmagick = require('./image/gmagick')
const ffmpeg = require('./video/ffmpeg')
const ffargs = require('./video/ffargs')

const GIF_FILE = /\.gif$/i
const DEFAULT_PHOTO_QUALITY = 90 // percent
const DEFAULT_AUDIO_BITRATE = '96k'
const DEFAULT_VIDEO_FPS = 25

/*
Convert and/or resize an image
Expand All @@ -30,45 +27,8 @@ exports.image = function (source, target, options, callback) {
}
}

// start processing with GraphicsMagick
const image = gm(source)

// read baked-in orientation info, and output a rotated image with orientation=0
image.autoOrient()

// optional watermark
const cropping = options.width && options.height
if (options.watermark && !cropping) {
image.composite(options.watermark.file)
if (options.watermark.position === 'Repeat') {
image.tile(options.watermark.file)
} else if (typeof options.watermark.position === 'string') {
image.gravity(options.watermark.position)
} else {
image.gravity('SouthEast')
}
}

// resize if necessary
if (cropping) {
// crop to the exact height and weight
image.resize(options.width, options.height, '^')
image.gravity('Center')
image.crop(options.width, options.height)
image.out('+repage')
} else if (options.height) {
// resize to a maximum height
image.resize(null, options.height, '>')
} else if (options.width) {
// resize to a maximum width
image.resize(options.width, null, '>')
}

// default quality, for typical web-friendly sizes
image.quality(options.quality || DEFAULT_PHOTO_QUALITY)
// apply custom post-processing arguments (sharpen, brightness...)
gmargs.apply(image, options.args)
// write the output image
// process the image with GraphicsMagick
const image = gmagick.prepare(source, options)
image.write(target, callback)
}

Expand All @@ -80,33 +40,8 @@ exports.video = function (source, target, options, callback) {
// create target folder if needed
mkdirp.sync(path.dirname(target))

// common options
const args = ['-i', source, '-r', DEFAULT_VIDEO_FPS, '-vsync', '2', '-movflags', '+faststart', '-ab', DEFAULT_AUDIO_BITRATE]

// output to mp4 or webm which are well read on the web
if (options.format === 'webm') {
args.push('-f', 'webm', '-vcodec', 'libvpx-vp9', '-strict', '-2')
} else {
args.push('-f', 'mp4', '-vcodec', 'libx264')
}

// set average bitrate or perceptual quality
if (options.bitrate) {
args.push('-b:v', options.bitrate)
} else {
// TODO: add configurable CRF value
args.push('-b:v', 0, '-crf', 20)
}

// AVCHD/MTS videos need a full-frame export to avoid interlacing artefacts
if (path.extname(source).toLowerCase() === '.mts') {
args.push('-vf', 'yadif=1')
}

// target filename
args.push('-y', target)

// return a EventEmitter to follow progress, since this can take a long time
// run ffmpeg to create the downsized video
const args = ffargs.prepare(source, target, options)
return ffmpeg.exec(args, callback)
}

Expand Down
44 changes: 44 additions & 0 deletions lib/video/ffargs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const path = require('path')

const DEFAULT_AUDIO_BITRATE = '96k'
const DEFAULT_VIDEO_FPS = 25
// const ENCODER_CRF_MAX = { h264: 51, vpx: 63 }

exports.prepare = function (source, target, options) {
// source file
const args = ['-i', source]

// output framerate
args.push('-r', DEFAULT_VIDEO_FPS)

// misc options
args.push('-vsync', '2', '-movflags', '+faststart')

// audio bitrate
args.push('-ab', DEFAULT_AUDIO_BITRATE)

// output to mp4 or webm which are well read on the web
if (options.format === 'webm') {
args.push('-f', 'webm', '-vcodec', 'libvpx-vp9', '-strict', '-2')
} else {
args.push('-f', 'mp4', '-vcodec', 'libx264')
}

// set average video bitrate or perceptual quality
if (options.bitrate) {
args.push('-b:v', options.bitrate)
} else {
// TODO: add configurable CRF value
args.push('-b:v', 0, '-crf', 20)
}

// AVCHD/MTS videos need a full-frame export to avoid interlacing artefacts
if (path.extname(source).toLowerCase() === '.mts') {
args.push('-vf', 'yadif=1')
}

// target filename
args.push('-y', target)

return args
}
File renamed without changes.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Convert / resize / transcode photos & videos to be web-friendly",
"main": "lib/index.js",
"scripts": {
"test": "tape test/*.js | tap-spec",
"test": "tape test/**/*.js | tap-spec",
"pretest": "standard && require-lint"
},
"author": "thumbsup",
Expand Down
14 changes: 7 additions & 7 deletions test/gmargs.js → test/image/gmagick.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const gmargs = require('../lib/gmargs')
const gmagick = require('../../lib/image/gmagick')
const sinon = require('sinon')
const tape = require('tape')

Expand All @@ -11,23 +11,23 @@ function gm () {
tape('no arguments', t => {
t.plan(1)
const image = gm()
gmargs.apply(image, undefined)
gmagick.addRawArgs(image, undefined)
t.equal(image.out.callCount, 0)
t.end()
})

tape('empty array of arguments', t => {
t.plan(1)
const image = gm()
gmargs.apply(image, [])
gmagick.addRawArgs(image, [])
t.equal(image.out.callCount, 0)
t.end()
})

tape('single argument with no values', t => {
t.plan(2)
const image = gm()
gmargs.apply(image, ['-equalize'])
gmagick.addRawArgs(image, ['-equalize'])
t.equal(image.out.callCount, 1)
t.deepEqual(image.out.args[0], ['-equalize'])
t.end()
Expand All @@ -36,7 +36,7 @@ tape('single argument with no values', t => {
tape('single argument with one value', t => {
t.plan(2)
const image = gm()
gmargs.apply(image, ['-modulate 120'])
gmagick.addRawArgs(image, ['-modulate 120'])
t.equal(image.out.callCount, 1)
t.deepEqual(image.out.args[0], ['-modulate', '120'])
t.end()
Expand All @@ -45,7 +45,7 @@ tape('single argument with one value', t => {
tape('single argument with space-separated values', t => {
t.plan(2)
const image = gm()
gmargs.apply(image, ['-unsharp 2 0.5 0.5 0'])
gmagick.addRawArgs(image, ['-unsharp 2 0.5 0.5 0'])
t.equal(image.out.callCount, 1)
t.deepEqual(image.out.args[0], ['-unsharp', '2 0.5 0.5 0'])
t.end()
Expand All @@ -54,7 +54,7 @@ tape('single argument with space-separated values', t => {
tape('multiple arguments', t => {
t.plan(2)
const image = gm()
gmargs.apply(image, [
gmagick.addRawArgs(image, [
'-equalize',
'-modulate 120',
'-unsharp 2 0.5 0.5 0'
Expand Down
2 changes: 1 addition & 1 deletion test/diff.js → test/integration/diff.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const _ = require('lodash')
const childProcess = require('child_process')
const gm = require('gm')
const convert = require('../lib/index')
const convert = require('../../lib/index')

// process an image, and compare to the expected output
exports.image = function (test, args) {
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion test/video.js → test/integration/video.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const assert = require('assert')
const tape = require('tape')
const diff = require('./diff')
const convert = require('../lib/index')
const convert = require('../../lib/index')

tape('can downsample a video for a smaller filesize', (test) => {
diff.video(test, {
Expand Down
2 changes: 1 addition & 1 deletion test/ffmpeg.js → test/video/ffmpeg.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const assert = require('assert')
const tape = require('tape')
const childProcess = require('child_process')
const ffmpeg = require('../lib/ffmpeg')
const ffmpeg = require('../../lib/video/ffmpeg')
const mockSpawn = require('mock-spawn')
const sinon = require('sinon')

Expand Down

0 comments on commit 0b881dd

Please sign in to comment.