Wednesday 13th of July 2016

As mentioned in a previous post, I play Guild Wars 2, and like to take a lot of screenshots. The game client has a few annoying limitations though. First off, it can only output JPEG or Windows Bitmap (BMP) format; I’ve set it to BMP for highest quality, but Capture One 9, which I use for editing, doesn’t support this format, necessitating a conversion to something more suitable.

The second problem is that the client can only write 999 files to a single folder (named gw001.bmpgw999.bmp). When it gets to 999, it won’t write any more, and just outputs an error in the chat window when you try to take a screenshot.

If you use something like ShadowPlay or ReShade, possibly you have other ways around these issues, I don’t know, I haven’t felt the need to use anything like that. Besides, writing scripts to automate things is fun!

There’s a further complicating factor here: the computer I play the game on is not the same as the one I do the editing on, so I use Dropbox to sync between the two. Since you can’t (as far as I know) change where the game client stores its screenshots, I’ve created a ‘GW2Screens’ folder in my Dropbox, then I’ve used the Windows mklink tool to create a symbolic link to it in the Guild Wars 2 folder in Documents:

mklink /d screens C:\Users\Andy\Dropbox\GW2Screens

This way any screenshots I take are automatically backed up by Dropbox.

To work around the two problems above, I wrote a Node.js script that, via ImageMagick, takes each BMP file, makes a PNG version of it, and names the resulting file using a 6-digit serial number. One million should be plenty of available names, but even if it’s not, it just means numbers over 999 999 won’t be zero-padded.

One nice side-effect of the process is that since the original BMP files are deleted after conversion, the game client always starts numbering back at 001, so I’ll basically never have to worry about hitting the 999 limitation again.

Anyway, here’s the script, written in mostly ES5 but with a few ES6 features. The chalk module is a handy thing for controlling console output colour. I’ve tried it on Mac OS X 10.11 and Windows 10, and as long as you have a working Node setup and the relevant dependencies installed it should be fine.

#!/usr/local/bin/node
//jshint node:true, esversion: 6
'use strict';
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const spawn = require('child_process').spawnSync;
const spawnAsync = require('child_process').spawn;

const CWD = process.cwd();

function extractNumber(filename) {
    const match = filename.match(/\d+/);
    return match ? parseInt(match[0], 10) : NaN;
}

function byNumber(a, b) {
    const x = extractNumber(a.name);
    const y = extractNumber(b.name);

    return x - y;
}

function pad(num, digits) {
    const str = num.toString();
    const padding = [];
    padding.length = Math.max(digits - str.length + 1, 0);
    return padding.join('0') + str;
}

const files = fs.readdirSync(CWD).reduce(function (list, filename) {
    const ext = filename.match(/\.(\w+)$/);
    const fullName = path.join(CWD, filename);

    if (!fs.lstatSync(fullName).isDirectory()) {
        list.push({
            'name': filename,
            fullName: fullName,
            extension: ext ? ext[1] : ''
        });
    }

    return list;
}, []).sort(function (a, b) {
    const x = a.name.toLowerCase();
    const y = b.name.toLowerCase();

    return x < y ? -1 : 1;
});

const bmpFiles = files.filter(function (file) {
    return file.extension.toLowerCase() === 'bmp';
});

const lastPngFile = files.filter(function (file) {
    return file.extension.toLowerCase() === 'png';
}).sort(byNumber).pop();

const totalFiles = bmpFiles.length;

let numConverted = 0;
let numErrors = 0;
let nextIndex = lastPngFile ? extractNumber(lastPngFile.name) + 1 : 0;

if (totalFiles === 0) {
    console.log('No .bmp files found, exiting.');
    process.exit();
}

console.log('Converting', totalFiles, '.bmp files...');
bmpFiles.forEach(function (file, index) {
    const targetName = 'gw' + pad(nextIndex, 6) + '.png';

    process.stdout.write(`${chalk.blue(file.name)} → ${chalk.blue(targetName)} ( ${index + 1} / ${totalFiles} )... `);

    const result = spawn('mogrify', [
        '-format',
        'png',
        '-write',
        targetName,
        file.name]);

    console.log(result.status === 0 ? chalk.green('✔') : chalk.red('✘'));

    if (result.status === 0) {
        numConverted += 1;
        fs.unlinkSync(file.fullName);
    } else {
        numErrors += 1;
    }
    nextIndex += 1;
});

console.log(
    chalk.green(numConverted),
    'files converted successfully,',
    chalk[numErrors > 0 ? 'red' : 'yellow'](numErrors),
    'errors'
);

let sayProcess;

if (numErrors > 0) {
    sayProcess = spawnAsync('say', ['-r', '300', numConverted + ' BMP files converted, ' +
        numErrors + ' errors.']);
} else {
    sayProcess = spawnAsync('say', ['-r', '300', numConverted + ' BMP files converted']);
}

// Don't really care about the error, just means 'say' doesn't
// exist and user doesn't get nice audio notification.
sayProcess.on('error', function (err) {});

I also made a super simple Windows batch file, since Windows doesn’t let you directly execute JavaScript files via node like Unixy OSes do:

node %USERPROFILE%\code\topng.js

(assuming you have the .js file in code\ in your home directory)

Happy to answer any questions you have in the comments, or if you like, in-game at Caer.1605.