Convert JPG's into PNG's using Node.js and ImageMagick - Possible Photo workflow?

Date: Tue Jun 20 2017 ImageMagick »»»» Image Processing

I'm pondering a new photography workflow where, instead of working with the JPG's directly out of the camera, that I instead convert them to PNG first.  The reasoning is that JPG is a lossy image format meaning every time you edit a JPG it loses a bit of precision, whereas PNG is a lossless image format and you don't lose precision.  That idea might not be the best, I haven't checked how well Picasa works with PNG's, in any case it let me play with what kind of image manipulation we can do in Node.js.

Let's first talk a little about image processing in Node.  This task obviously requires a function for converting JPG to PNG, but I'm interested in other abilities such as inserting watermarks or resizing images.  There is no built-in capability to operate on images in Node, meaning we're reliant on packages in NPM.   Typing in "npm search image" and "npm search jpeg" gives us a list of those packages.

Some of the packages can be dispensed right away - because they're performing functions needed for web applications, such as generating a sprite-filled image, or geared up to live inside a Connect application, or interfacing with various websites.  After some trials I settled on the imagemagick package which wraps around ImageMagick, an application I'd never used before but now that I've read up on it, it's a pretty impressive tool.  ImageMagick, if a bit clunky, is feature-filled and will let me do what I want.

But let's get back to discussing how I implemented the task at hand.  What I want is, given a pathname for a directory containing JPG files, create a directory next to it containing PNG files.  The process is fairly simple, for each file in the first directory run the convert program giving it a result filename with a .png suffix in the PNG directory.

Here's the code

var im    = require('imagemagick');
var fs = require('fs');
var util = require('util');
var async = require('async');

var dir = process.argv[2];

fs.mkdirSync(dir+'-png', 0755);

fs.readdir(dir, function(err, files) {
if (err) throw err;
async.filter(files, function(file, done) {
if (file.match(/[jJ][pP][gG]$/)) { done(true); }
async.forEachSeries(files, function(file, done) {
var pngfn = file.replace(/^(.*\.)[jJ][pP][gG]$/, '$1') + "png";
console.log('converting ' + file + ' to ' + pngfn);
im.convert(['-verbose', dir + '/' + file, dir + '-png/' + pngfn],
function(err, stdout) {
if (err) done(err);
console.log('stdout:', stdout);
}, function(err) {
// if any of the saves produced an error, err would equal that error

First step is we're given the directory name, and create a new directory by appending "-png" to the directory name.  If the "-png" directory already exists the script with throw an EEXIST error because, well, the directory already exists.  While the error message looks ugly it is an effective way to inform the user that the directory already exists.

We're using functions from the async package to de-asynchronize the asynchronous processing.  The callback to imagemagick.convert gets called when the conversion is done, and we want to ensure there is only one conversion running at a time if only to limit CPU load on your laptop.

The async.filter function lets us neatly select only the file names ending in JPG, and we know the resulting files array only has files ending with JPG.

Then, using async.forEachSeries ensures, as I said a minute ago, that there's only one conversion running at a time.  This function differs from async.forEach by making sure not to execute a loop body the previous loop body is finished (called "done").

The im.convert function, like other functions in the imagemagick package, uses its command line options to generate a command line and then invoke a shell command in a child process.

Then run it like so:

$ node jpg2png.js 2011-10-20
converting DSCN8246.JPG to DSCN8246.png
stdout: writing raw profile: type=8bim, length=136
writing raw profile: type=APP1, length=385
writing raw profile: type=exif, length=10506
writing raw profile: type=iptc, length=95
writing raw profile: type=xmp, length=667

converting DSCN8247.JPG to DSCN8247.png
stdout: writing raw profile: type=APP1, length=385
writing raw profile: type=exif, length=11118

converting DSCN8248.JPG to DSCN8248.png
stdout: writing raw profile: type=APP1, length=385
writing raw profile: type=exif, length=11226

converting DSCN8249.JPG to DSCN8249.png
stdout: writing raw profile: type=APP1, length=385
writing raw profile: type=exif, length=10976

There's a bit of a pause between the "converting" line and the "sdtout" line printed when the conversion is completed.