エクセル内画像を抜き出してkintoneに登録

kintoneって[全文検索](https://kintone.cybozu.com/jp/feature/application.html#search)という便利な機能があるんですよ。
エクセル業務のkintone乗り換えも多いんですよ。

乗り換え時に、全文検索もあるし、とりあえず手元にあるエクセルを全部登録しとくか！
という話もあると思うんですよ・・・多分・・・きっとどこかに・・・

ということで（？）、昔のデータを探す時にエクセルをダウンロードすることなく、kintone上でエクセル内の画像だけでも見られたら便利だなと思い立ち・・・思い立ったが吉日！
まずは[developer network](https://cybozudev.zendesk.com/hc/ja)などで似た記事がないか確認 φ(・ω・ )
なかったのでエクセルファイル内の画像をkintoneに登録するサンプルプログラムを作ってみました。

# できること
kintoneに添付されたエクセル内の画像を添付ファイルとして登録する。
文字データは未対応ですが、もう少し頑張ればできると思います。

# 出来上がりのイメージ
今回は結果から！
↓のように2シートに計5枚の画像が挿入されているエクセルで試してみると・・・
![excel5.jpg](https://qiita-image-store.s3.amazonaws.com/0/102790/22a96204-6073-e454-5151-7f8b4fa18c21.jpeg)

上手く行きました！（画面が灰色になっているのはリロードのため）
[![https://gyazo.com/ca147339e435c40b59bab856dd2829ba](https://i.gyazo.com/ca147339e435c40b59bab856dd2829ba.gif)](https://gyazo.com/ca147339e435c40b59bab856dd2829ba)

エクセル5ファイル、計40個の画像でも正常に動作することを確認しています。

# 前提
- エクセルの形式はxlsx
- 画像の種類はJPG、GIF、PNG
- kintoneのフォーム設定は以下の通り

| フィールド名 | フィールドタイプ | フィールドコード |
|----------|----------------|------------------|
| エクセルファイル | 添付ファイル | excelFile |
| 画像ファイル | 添付ファイル | imageFile |
|  | スペース | button |
- JS/CSS設定は以下
 - https://js.cybozu.com/jquery/2.2.0/jquery.min.js
 - https://js.cybozu.com/jszip/v2.5.0/jszip.min.js
 - ↓のJSファイル（ボタンを少し綺麗にする場合はCSSファイルも）

```js:sample.js
(function() {
    'use strict';

    kintone.events.on('app.record.detail.show', parseExcel);

    function parseExcel(event) {
        var button = kintone.app.record.getSpaceElement('button');
        $(button).append('<button id="parseExcelButton">エクセルファイル内の画像をkintoneに登録！</button>');
        $(button).append('<div id="parseExcelMessage"></div>');

        var record = event.record;

        $('#parseExcelButton').on('click', function(e) {
            $(this).prop('disabled', true);
            e.preventDefault();

            var files = record.excelFile.value;
            var excelFileKeys = [];
            for (var i = 0; i < files.length; i++) {
                if (files[i].contentType !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
                    continue;
                }
                excelFileKeys.push(files[i].fileKey);
            }
            if (excelFileKeys.length === 0) {
                $('#parseExcelMessage').text('エクセルファイルが登録されていません。');
                return false;
            }

            Promise.all(excelFileKeys.map(function(excelFileKey) {
                $('#parseExcelMessage').text('エクセルファイル取得中...');

                return getFile(excelFileKey);
            })).then(function(blobList) {
                console.log('[SUCCESS] get excel files.');

                $('#parseExcelMessage').text('エクセルファイル内の画像取得中...');

                // 2次元配列を1次元配列に
                var blobs = Array.prototype.concat.apply([], blobList);

                return Promise.all(blobs.map(function(blob) {
                    return pickupImages(blob);
                }));
            }).then(function(imageList) {
                console.log('[SUCCESS] get image files.');

                $('#parseExcelMessage').text('画像登録中...');

                var images = Array.prototype.concat.apply([], imageList);
                return Promise.all(images.map(function(image) {
                    return uploadImage(image);
                }));
            }).then(function(fileKeyList) {
                console.log('[SUCCESS] upload image files.');

                $('#parseExcelMessage').text('レコード更新中...');

                var fileKeys = Array.prototype.concat.apply([], fileKeyList);
                return updateRecord(fileKeys);
            }).then(function() {
                location.reload();
            }).catch(function(error) {
                console.log('[ERROR]', error);
            });
        });
    }

    // kintoneに添付されたファイルをダウンロード
    function getFile(fileKey) {
        var url = '/k/v1/file.json?fileKey=' + fileKey;
        var df = new $.Deferred();
        var xhr = new XMLHttpRequest();

        xhr.open('GET', url, true);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.responseType = 'blob';

        xhr.onload = function(e) {
            if (this.status === 200) {
                df.resolve(this.response);
            }
        };

        xhr.send();
        return df.promise();
    }

    // エクセルファイル内の画像を取得
    function pickupImages(blob) {
        var df = new $.Deferred();
        var reader = new FileReader();
        reader.readAsDataURL(blob);

        // .xlsxファイルの読み込み
        reader.onload = function(e) {
            var xlsxBase64 = e.target.result.split(',')[1];

            var zip = new JSZip();
            zip = zip.load(xlsxBase64, {base64: true});
            var mediaFiles = zip.folder('xl/media').file(/^image/);

            var images = [];
            for (var i = 0; i < mediaFiles.length; i++) {
                var filePath = mediaFiles[i].name;
                var fileName = filePath.replace(/.*\//, '');
                var fileMime = toMimeType(filePath);

                var arrayBuffer = zip.file(filePath).asArrayBuffer();
                var imageBlob = new Blob([arrayBuffer], {type: fileMime});
                images[i] = {'name': fileName, 'blob': imageBlob};
            }

            df.resolve(images);
        };

        return df.promise();
    }

    // 画像をkintoneに登録し、fileKeyを返す
    function uploadImage(image) {
        return new Promise(function(resolve, reject) {
            var formData = new FormData();
            formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());
            formData.append('file', image.blob, image.name);

            var xhr = new XMLHttpRequest();
            xhr.open('POST', '/k/v1/file.json', true);
            xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
            xhr.onload = function(e) {
                if (this.status === 200) {
                    var res = JSON.parse(this.responseText);
                    resolve(res.fileKey);
                } else {
                    reject(JSON.parse(this.responseText));
                }
            };

            xhr.send(formData);
        });
    }

    // レコード更新
    function updateRecord(fileKeys) {
        var appId = kintone.app.getId();
        var recId = kintone.app.record.getId();

        var putData = {
            'app': appId,
            'id': recId,
            'record': {
                'imageFile': {
                    'value': []
                }
            }
        };

        for (var i = 0; i < fileKeys.length; i++) {
            putData.record['imageFile'].value.push({'fileKey': fileKeys[i]});
        }

        return kintone.api('/k/v1/record', 'PUT', putData);
    }

    // MimeTypeを返す
    function toMimeType(filePath) {
        var reg = /(.*)(?:\.([^.]+$))/;
        var fileExt = filePath.match(reg)[2];

        var mimeType = '';
        switch (fileExt.toLowerCase()) {
            case 'jpg':
                mimeType = 'image/jpeg';
                break;
            case 'jpeg':
                mimeType = 'image/jpeg';
                break;
            case 'png':
                mimeType = 'image/png';
                break;
            case 'gif':
                mimeType = 'image/gif';
                break;
        }

        return mimeType;
    }

})();
```

```css:sample.css
button#parseExcelButton{
    display: block;
    padding: 10px;
    margin: 20px;
    text-align: center;
    border: 3px solid;
    border-color: #ccc #999 #999 #ccc;
    color: #666
}

button#parseExcelButton:hover{
    background: #aaa;
    color: #fff;
}

div#parseExcelMessage{
    display: block;
    padding: 10px;
    margin: 20px;
    text-align: center;
}
```

# kintone開発者ライセンス
kintone面白そうだな～と思った方は5ユーザー、1年間無償の[開発者ライセンス](https://cybozudev.zendesk.com/hc/ja/)を使ってみてください！