Part of #20, auto-rotates photos based on the EXIF data

pull/21/head
Romain Prieto 10 years ago
parent 357ae588c1
commit cb835d0ed2

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

@ -27,14 +27,14 @@
"thumbsup": "./bin/thumbsup.js"
},
"dependencies": {
"handlebars": "~2.0.0",
"lodash": "~2.4.1",
"async": "~0.7.0",
"exif-parser": "~0.1.6",
"fs-extra": "~0.8.1",
"gm": "~1.14.2",
"glob": "~3.2.9",
"async": "~0.7.0",
"gm": "~1.14.2",
"handlebars": "~2.0.0",
"lodash": "~2.4.1",
"pad": "~0.0.4",
"exif-parser": "~0.1.6",
"progress": "~1.1.5",
"yargs": "~1.2.6"
}

@ -25,6 +25,10 @@ exports.build = function(opts) {
}
}
function copyFile(task, callback) {
fs.copy(task.src, task.dest, callback);
}
async.series([
function updateMetadata(callback) {
@ -39,7 +43,7 @@ exports.build = function(opts) {
message: 'Original photos',
ext: 'jpg|jpeg|png',
dest: '/original/$path/$name.$ext',
func: fs.copy
func: copyFile
}),
buildStep({
@ -47,7 +51,7 @@ exports.build = function(opts) {
message: 'Original videos',
ext: 'mp4|mov|mts',
dest: '/original/$path/$name.$ext',
func: fs.copy
func: copyFile
}),
buildStep({

@ -7,17 +7,17 @@ var ProgressBar = require('progress');
exports.exec = function(input, output, metadata, options, callback) {
var message = pad(options.message, 20)
var paths = Object.keys(metadata).filter(extension(options.ext));
var tasks = paths.map(function(p) {
var tasks = paths.map(function(relativePath) {
return {
relative: p,
absolute: path.join(input, p),
dest: path.join(output, transform(p, options.dest))
src: path.join(input, relativePath),
dest: path.join(output, transform(relativePath, options.dest)),
metadata: metadata[relativePath]
};
});
var process = tasks.filter(function(task) {
try {
var destDate = fs.statSync(task.dest).ctime.getTime();
return metadata[task.relative].fileDate > destDate;
return task.metadata.fileDate > destDate;
} catch (ex) {
return true;
}
@ -28,7 +28,7 @@ exports.exec = function(input, output, metadata, options, callback) {
var ops = process.map(function(task) {
return function(next) {
fs.mkdirpSync(path.dirname(task.dest));
options.func(task.absolute, task.dest, function(err) {
options.func(task, function(err) {
bar.tick();
next(err);
});

@ -58,22 +58,30 @@ exports.update = function(opts, callback) {
}
function metadata(fileInfo) {
var exifData = getExifData(fileInfo);
return {
path: fileInfo.relative,
fileDate: fileInfo.fileDate,
mediaDate: mediaDate(fileInfo),
mediaType: mediaType(fileInfo)
mediaType: mediaType(fileInfo),
mediaDate: exifData.date || fileInfo.fileDate,
orientation: exifData.orientation
};
}
function mediaDate(fileInfo) {
function getExifData(fileInfo) {
if (fileInfo.relative.match(/\.(jpg|jpeg)$/i)) {
var contents = fs.readFileSync(fileInfo.absolute);
var result = exif.create(contents).parse();
var exifDate = result.tags.DateTimeOriginal * 1000;
return exifDate || fileInfo.fileDate;
var date = result.tags.DateTimeOriginal;
return {
date: (date * 1000) || fileInfo.fileDate,
orientation: result.tags.Orientation || 1
};
} else {
return fileInfo.fileDate;
return {
date: fileInfo.fileDate,
orientation: 1
};
}
}

@ -9,28 +9,30 @@ exports.sizes = {
};
// Small square photo thumbnail
exports.photoSquare = function(src, dest, callback) {
gm(src)
.resize(exports.sizes.thumb, exports.sizes.thumb, '^')
.gravity('Center')
.crop(exports.sizes.thumb, exports.sizes.thumb)
.quality(90)
.write(dest, callback);
exports.photoSquare = function(task, callback) {
var img = gm(task.src);
img = rotatePhoto(img, task.metadata.orientation);
img.resize(exports.sizes.thumb, exports.sizes.thumb, '^')
.gravity('Center')
.crop(exports.sizes.thumb, exports.sizes.thumb)
.quality(90)
.write(task.dest, callback);
};
// Large photo
exports.photoLarge = function(src, dest, callback) {
gm(src)
.resize(null, exports.sizes.large, '>')
.quality(90)
.write(dest, callback);
exports.photoLarge = function(task, callback) {
var img = gm(task.src);
img = rotatePhoto(img, task.metadata.orientation);
img.resize(null, exports.sizes.large, '>')
.quality(90)
.write(task.dest, callback);
};
// Web-streaming friendly video
exports.videoWeb = function(src, dest, callback) {
var ffmpeg = 'ffmpeg -i "' + src + '" -y "'+ dest +'" -f mp4 -vcodec libx264 -ab 96k';
exports.videoWeb = function(task, callback) {
var ffmpeg = 'ffmpeg -i "' + task.src + '" -y "'+ task.dest +'" -f mp4 -vcodec libx264 -ab 96k';
// AVCHD/MTS videos need a full-frame export to avoid interlacing artefacts
if (path.extname(src).toLowerCase() === '.mts') {
if (path.extname(task.src).toLowerCase() === '.mts') {
ffmpeg += ' -vf yadif=1 -qscale:v 4';
} else {
ffmpeg += ' -vb 1200k';
@ -39,15 +41,33 @@ exports.videoWeb = function(src, dest, callback) {
};
// Large video preview (before you click play)
exports.videoLarge = function(src, dest, callback) {
var ffmpeg = 'ffmpeg -itsoffset -1 -i "' + src + '" -ss 0.1 -vframes 1 -y "' + dest + '"';
exports.videoLarge = function(task, callback) {
var ffmpeg = 'ffmpeg -itsoffset -1 -i "' + task.src + '" -ss 0.1 -vframes 1 -y "' + task.dest + '"';
exec(ffmpeg, callback);
};
// Small square video preview
exports.videoSquare = function(src, dest, callback) {
exports.videoSquare = function(task, callback) {
async.series([
exports.videoLarge.bind(this, src, dest),
exports.photoSquare.bind(this, dest, dest)
exports.videoLarge.bind(this, task),
exports.photoSquare.bind(this, {
src: task.dest,
dest: task.dest,
metadata: {}
})
], callback);
};
// Many browsers don't display EXIF orientation properly
// We need to rotate all photos so their orientation is back to 1
function rotatePhoto(img, orientation) {
if (orientation === 1) return img;
if (orientation === 2) return img.flop();
if (orientation === 3) return img.rotate("black", 180);
if (orientation === 4) return img.flip();
if (orientation === 5) return img.rotate("black", 90).flop();
if (orientation === 6) return img.rotate("black", 90);
if (orientation === 7) return img.rotate("black", 270).flop();
if (orientation === 8) return img.rotate("black", 270);
return img;
}

Loading…
Cancel
Save