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

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

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

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

プロジェクト作成

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

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

1
2
mkdir iconc
npm init

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

1
2
3
4
npm install async --save
npm install fs-extra --save
npm install imagemagick --save
npm install colors --save

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

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

1
2
npm install mocha --save-dev
npm install should --save-dev

テストから作る

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
'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のテストコマンドに登録しときます:

1
2
3
4
5
6
// package.json
    ...,
    "scripts": {
        "test": "NODE_ENV=testing node ./node_modules/.bin/mocha ./test/index.test.js --async-only"
    },
    ...

npm testを実行してみると:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
'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を走らせると:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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/