paper.cssを使ってブラウザだけでExcelのデータを差し込み印刷する


## この記事について

[前回](https://qiita.com/okoppe8/items/abcafdad3a894bca7f38)の続きとして、ブラウザでOfficeの差し込み印刷を再現するサンプル(宛名ラベル印刷)を作成しました。

入力データにはExcelファイルを使います。
「A-ONE ラベルシール 12面付（幅86.4mmx高さ42.3mm 2列6段）」用です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/3b24878e-453e-0ba8-364f-bf10258e53a1.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/3c91f3b3-5140-0641-262a-39c20a50192c.png)


サンプルアプリはこちら
https://okoppe8.github.io/merge_printing/index.html

github リポジトリ
https://github.com/okoppe8/merge_printing

## サンプルアプリの使い方

まずひな形ファイルをダウンロードしてください。
すでにサンプルデータが入っていますが、必要であれば編集し下のファイル入力で読み込みませてください。印刷ボタンで別ウィンドウの印刷画面が立ち上がります。

ちなみに全てブラウザで処理が完結しているので、ファイルを選択してもアップロードはしていません。

## ブラウザで印刷業務をするメリット

- Web開発の技術（HTML/CSS）がそのまま使える。

- 印刷内容の動的な変更が容易（賞状の色を得点によって変えるなど）

- Webサービスにすると、ファイル内に印刷イメージを置かないのでデザインの変更が即座に反映される。

- バージョン管理が容易。

- Officeの差し込み印刷は操作が複雑。Webページなら説明も書けて流れ作業としてやってもらえる。

- この手法でExcelファイルをデータソースに使うと、データベースが不要になる。データ流出の心配がなくなる。

JavaScriptで完結するのでローカルファイルとして開いても動作します（CDNのリンクの修正が必要）。
カジュアルな用途にもどんどん使っていけると思います。

## コードの説明と参考資料

ブラウザの帳票印刷をする方法については以下のエントリを参照してください。

- そろそろ真面目に、HTMLで帳票を描く話をしようか
 - https://qiita.com/cognitom/items/d39d5f19054c8c8fd592

- paper.cssを使ってブラウザだけで名刺を印刷する
 - https://qiita.com/okoppe8/items/abcafdad3a894bca7f38 

ファイルの読み込みにはJavaScriptのライブラリ「SheetJS」を利用し、ExcelシートをJSONに変換しています。

- [SheetJS] WebアプリでExcelをデータベースとして使う
 - https://qiita.com/okoppe8/items/995b57d4e3d6d512b916

JSONの処理とHTMLのテンプレート処理にはunderscore.jsを使用しています。

- 遅すぎたUnderscore.js入門 - 全体像
 - https://qiita.com/hp0me/items/72d80dc166aace2759dc

- webpack + underscore.jsでテンプレートエンジン
 - https://qiita.com/kabane/items/eca8304bb311b3e948ba


## サンプルコード

メイン画面

```html:index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>ブラウザで差し込み印刷 サンプル</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
        crossorigin="anonymous">
    <link rel="stylesheet" href="css/app.css">
</head>

<body>
    <div class="container-fluid">
        <h2>宛名ラベルの作成</h2>
        <div class="row">
            <div class="col-12">
                <p class="description">１．Excelファイルをダウンロードします</p>
            </div>
        </div>
        <div class="row">
            <div class="col-12">
                <a class="btn btn-info" href="address_list.xlsx" download="宛先リスト.xlsx">ダウンロード</a>
            </div>
        </div>
        <div class="row">
            <div class="col-12">
                <p class="description">２．Excelファイルを編集後、以下より選択します。</p>
            </div>
        </div>
        <div class="row">
            <div class="col-sm-6">
                <div class="custom-file">
                    <input type="file" class="custom-file-input" id="customFile">
                    <label class="custom-file-label" for="customFile">ファイルを選択...</label>
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-12">
                <p class="description">３．印刷します</p>
            </div>
        </div>
        <div class="row">
            <div class="col-12">
                <a class="btn btn-primary disabled" href="#" id="print" style="margin-right:1em">印　刷</a>
                <label for="print" id="count"></label>
            </div>
        </div>
    </div>
    </div>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
        crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.11.19/xlsx.full.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
    <script>
        var X = XLSX;
        var datalist; // 読み込んだデータを配列で格納

        // Excelから読み込まれたJSONを配列に変換する
        function json_to_list(sheet_json) {
            // 最初の要素（１つめのシート）を取得
            raw_records = sheet_json[_.keys(sheet_json)[0]];
            // SheetJSの都合でキー名称が日本語になっているので、英語に直しておく。
            converted_records =
                _.map(raw_records,
                    function (old_obj) {
                        new_obj = new Object();
                        new_obj.name = old_obj.名前;
                        new_obj.zipcode = old_obj.郵便番号;
                        new_obj.address1 = old_obj.住所1;
                        new_obj.address2 = old_obj.住所2;
                        return new_obj;
                    });

            return converted_records;
        }

        // 印刷ボタン 有効/無効の制御
        function count_list() {
            if (Array.isArray(datalist) && datalist.length > 0) {
                $('#print').removeClass('disabled');
                $('#count').text('対象件数：' + datalist.length + '件')
            } else {
                $('#print').addClass('disabled');
                $('#count').text('対象件数：０件')
            }
        }

        // ファイルの読み込み
        function fixdata(data) {
            var o = "",
                l = 0,
                w = 10240;
            for (; l < data.byteLength / w; ++l) o += String.fromCharCode.apply(null, new Uint8Array(data.slice(l * w,
                l * w + w)));
            o += String.fromCharCode.apply(null, new Uint8Array(data.slice(l * w)));
            return o;
        }

        // ワークブックのデータをjsonに変換
        function to_json(workbook) {
            var result = {};
            workbook.SheetNames.forEach(function (sheetName) {
                var roa = X.utils.sheet_to_json(
                    workbook.Sheets[sheetName],
                    {
                        raw: true,
                    });
                if (roa.length > 0) {
                    result[sheetName] = roa;
                }
            });
            return result;
        }

        // ファイル選択時のメイン処理
        function handleFile(e) {

            var files = e.target.files;
            var f = files[0];

            // データの読み込み
            var reader = new FileReader();
            reader.onload = function (e) {
                var data = e.target.result;
                var wb;
                var arr = fixdata(data);
                wb = X.read(btoa(arr), {
                    type: 'base64',
                    cellDates: true,
                });

                // シートよりJSON作成
                jsondata = to_json(wb);
                // JSONより配列を作成
                datalist = json_to_list(jsondata)                
                console.log(datalist);
                // 件数取得、印刷ボタン 有効無効の制御。
                count_list();

            };
            reader.readAsArrayBuffer(f);
        }

        // 画面ロード時の処理
        $(document).ready(function () {

            // ファイル選択欄 選択イベント
            // http://cccabinet.jpn.org/bootstrap4/javascript/forms/file-browser
            $('.custom-file-input').on('change', function (e) {
                handleFile(e);
                $(this).next('.custom-file-label').html($(this)[0].files[0].name);
            })

            $('.custom-file-input').on('click', function (e) {
                $(this).val(null);
            })

            //// 印刷ボタンクリック時（印刷画面をオープンする）
            $('#print').on('click', function (e) {
                var url = 'print_image.html';
                window.open(url);

            })

        });

    </script>
</body>

</html>
```

印刷画面

```html:print_image.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>印刷画面</title>
    <link rel="stylesheet" href="css/paper.css">
    <link rel="stylesheet" href="css/app.css">
</head>

<body class="A4">
    <section class="sheet" id="originSheet">
        <table class="print">
            <tbody>
                <tr>
                    <td>
                        <div class="data">
                            <!-- 開発時確認用 -->
                            <p class="zipcode">123-4567</p>
                            <p class="address1">北海道札幌市大通１丁目 １－１</p>
                            <p class="address2">テレビタワーマンション１５７１号</p>
                            <p class="name">テレビ 塔三 様</p>
                        </div>
                    </td>
                    <td></td>
                </tr>
                <tr>
                    <td></td>
                    <td></td>
                </tr>
                <tr>
                    <td></td>
                    <td></td>
                </tr>
                <tr>
                    <td></td>
                    <td></td>
                </tr>
                <tr>
                    <td></td>
                    <td></td>
                </tr>
                <tr>
                    <td></td>
                    <td></td>
                </tr>
            </tbody>
        </table>
    </section>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
        crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
    <script type="text/javascript">
        $(document).ready(function () {

            // 親ウィンドウのデータを参照
            var datalist = window.opener.datalist;

            // 12枚を超える場合はページを増やす
            var sheets = Math.ceil(datalist.length / 12);
            _.times(sheets - 1, function () {
                $('BODY').append($('#originSheet').prop('outerHTML'))
            })

            // underscore.js のテンプレート機能でテーブルのセルにデータを流し込む
            var compiled = _.template('<div class="data"><p class="zipcode"><%= data.zipcode %></p>' +
                '<p class="address1"><%= data.address1 %></p>' +
                '<p class="address2"><%= data.address2 %></p>' +
                '<p class="name"><%= data.name %> 様</p></div>')

            var cell = $('table.print td');
            _.each(datalist, function (data, i) {
                $(cell[i]).html(compiled({
                    "data": data
                }));
            })

            // 印刷画面を自動的に表示。閉じると自動的にウィンドウも閉じる。
            setTimeout(function () {
                window.print();
                window.close();
            }, 200);
        })
    </script>
</body>

</html>

```