アイコンリサイズ用のnpmパッケージを作ってみた その1

ChainZ(クリエイター)
いろいろやってます。

cocos2d-xのプロジェクトのアイコンサイズが異常に多いので、node.jsで自動リサイズのツールを作ってみました。

実はデザインツールのSketchはサイズごとにexportするという神の機能があるので、デザイナーの方はそちらを使ったほうが幸せになります。

プロジェクト作成

iconcというフォルダーを作成し、npm initでパッケージの初期化します:

パッケージ名はなんとなくiconcにしました。特に深い意味はないです。

mkdir iconc
npm init

とりあえず、package.jsonversion0.1.0にしときます。必要な依存パッケージをdependenciesに追加する:

npm install async --save
npm install fs-extra --save
npm install imagemagick --save
npm install colors --save

asyncは非同期コードを書く重宝のライブラリです。fs-extraは公式APIfsより便利に拡張したライブラリです。imagemagickは画像のリサイズ用です。

テストツールはmochashouldを使います:

npm install mocha --save-dev
npm install should --save-dev

テストから作る

自分流のやり方ですが、とりあえずテストコードを書いて、このライブラリはどんな風に使われるかをtest/index.test.jsに書いてみます:

'use strict';
const mocha = require('mocha');
const should = require('should');
const path = require('path');
const Iconc = require('../');
const fs = require('fs-extra');
// 処理対象のファイル
const FILE = path.join(__dirname, './file/_icon_.png');
describe('iconc', () => {
it('generate icons', (done) => {
// リサイズされた画像を./test/file/genフォルダーに入れる
const dest = path.join(__dirname, 'file/gen');
// 置き場は毎回リセットする
fs.removeSync(dest);
fs.ensureDirSync(dest);
// こんな感じで、サイズを決める:wはwidthで、pはpercentageという意味
const schema = {
'icon-40': { w: 40 },
'icon@2x': { p: 200 }
};
// instanceを作って...
const iconc = new Iconc({
file: FILE,
schema: schema,
dest: dest
});
// 実行
iconc.run(err => {
if (err) return done(err);
// 結果を検証
// TODO: サイズも検証すべき
Object.keys(schema).forEach(key => {
fs.existsSync(path.join(__dirname, `file/gen/${key}.png`)).should.be.true();
});
return done();
});
});
});

npmのテストコマンドに登録しときます:

// package.json
...,
"scripts": {
"test": "NODE_ENV=testing node ./node_modules/.bin/mocha ./test/index.test.js --async-only"
},
...

npm testを実行してみると:

もちろん、Iconcというオブジェクトは作ってないから、エラーになります。プロジェクトのルートにindex.jsを作成し、Iconcというクラスを作ります:

'use strict';
const path = require('path');
const async = require('async');
const assert = require('assert');
function Iconc(params) {
params = params || {};
this.file = params.file;
this.dest = params.dest;
this.schema = params.schema;
assert(this.file, 'params.file is required');
assert(this.dest, 'params.dest is required');
assert(this.schema, 'params.schema is required');
// schemaはファイルのパスも受付するので、stringタイプも渡せる
assert(['string', 'object'].indexOf(typeof(this.schema)) >= 0, 'schema should be a file path or object');
// 今cdしているパスを基準にする
// そうすると、コマンドラインで引数に相対パスも渡せるようになる
this.dest = path.resolve(process.cwd(), this.dest);
this.file = path.resolve(process.cwd(), this.file);
}
Iconc.prototype.run = function(done) {
return done();
};
module.exports = Iconc;

もう一度npm testを走らせると:

エラーメッセージが変わりました。今回はファイル生成されてないので、テストが通らないわけです。リサイズの実装を進めます:

Iconc.prototype.run = function(done) {
const self = this;
async.waterfall([
// 対象のファイルが存在するか?存在しない場合はエラーを返す
(done) => fs.exists(self.file, exists => exists ? done() : done(new Error(`not found: ${self.file}`))),
(done) => {
// schemaがstringタイプの場合はファイルパスと認識し、読み込みの試しをする
if (typeof(self.schema) == 'string') {
return fs.exists(self.schema, exists => {
if (!exists) {
return done(new Error(`schema: ${self.schema} not found`));
}
self.schema = path.resolve(process.cwd(), self.schema);
// yamlの場合
// js-yamlというライブラリを利用しています:npm install js-yaml --saveで追加
if (self.schema.endsWith('.yaml') || self.schema.endsWith('.yml')) {
self.schema = Yaml.safeLoad(fs.readFileSync(self.schema));
// JSON
} else if (self.schema.endsWith('.json')) {
self.schema = JSON.parse(fs.readFileSync(self.schema));
// 他のファイルは直接requireします
} else {
self.schema = require(self.schema);
}
return done();
})
}
done();
},
// 対象画像の情報を取得
(done) => im.identify(self.file, done),
(info, done) => {
// ディプロイ先を確保(なければ作成する)
fs.ensureDirSync(self.dest);
// 全て順番処理する
async.series(Object.keys(self.schema).map(name => {
return (done) => {
const s = self.schema[name];
const dst = path.join(self.dest, `${name}${path.extname(self.file)}`);
const opt = {
srcPath: self.file,
dstPath: dst
};
// wキーがある場合はwidthを設定
if (s.w) {
opt.width = s.w;
}
// pキーの場合はパーセンテージにする
if (s.p) {
opt.width = info.width * s.p/100;
}
if (!opt.width && !opt.height) {
return done(new Error('invalid schema'));
}
// リサイズ
im.resize(opt, err => {
if (err) return done(err);
return done(null, dst);
});
}
}), done);
}
], (err, files) => {
if (err) return done(err);
return done();
});
};

npm testを叩いてみると:

通りました!一応機能としてはできましたね! 今度はこの機能をコマンドツールとして使えるようにします。ではでは〜

次の記事:http://befool.co.jp/blog/chainzhang/creating-npm-package-2/