Web上のボタンを押すと指定範囲を印刷自動化

# 展示会での物理的な思い出

パラメータいじるだけでグラフィックを作成できる展示に物理的な思い出を持たせたい！！！！

クリエイティブコーディングやWeb上で作成したグラフィック、
それをポストカードやステッカーなどで持ち帰る体験を作りたいなと思い

ボタンを押しただけで自動で印刷まで出来上がる仕組みを構築しました
以下はその簡易実装例です

## 概要

本記事では、Webページ上のボタンをクリックすることで指定範囲をキャプチャし、その画像をサーバーに送信して自動で印刷する仕組みを実装する手順を紹介します。使用技術や環境構築、各コード例を含めて解説します。

### 使用技術

- HTML2Canvas
- Node.js v18.17.0 （Express, Multer）
- ShellScript

### 簡単な流れ

1. HTML2Canvasで指定範囲を保存
2. 画像データをサーバに送る
3. クライアントから送信されたデータを受け取る
5. シェルスクリプトをリクエスト
5. 印刷を実行

## 実装
### 環境構築

`Node.js` が入ってない方はこちらを参考にしてください。

https://qiita.com/qulylean/items/0ad521885a04a5ebd202

今回使う、`Express`, `Multer` をインポート
```terminal:ターミナル
npm install express multer
```
※ HTML2CanvasはCDNを使用

### フロント実装
↓実際の画面
![スクリーンショット 2024-08-06 19.39.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616213/9aff37ed-909c-2372-9a87-0742f9fa9c33.png)


```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>印刷テスト</title>
    <style>
        #captureArea {
            width: 300px;
            height: 200px;
            border: 1px solid #000;
            background-color: #d7a7a7;
            text-align: center;
            line-height: 200px;
        }
    </style>
</head>
<body>
    <div id="captureArea">
        ここが印刷されます
    </div>
    <button id="saveButton">画像を保存&印刷</button>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
    <script>
        document.getElementById('saveButton').addEventListener('click', () => {
            const captureArea = document.getElementById('captureArea');

            html2canvas(captureArea).then(canvas => {
                const dataURL = canvas.toDataURL('image/png');
                const blob = dataURLToBlob(dataURL);
                const formData = new FormData();
                formData.append('image', blob, 'capture.png');

                fetch('/save-and-print', {
                    method: 'POST',
                    body: formData
                }).then(response => {
                    if (response.ok) {
                        alert('保存&印刷DONE');
                    } else {
                        alert('error');
                    }
                });
            });
        });

        function dataURLToBlob(dataURL) {
            const parts = dataURL.split(';base64,');
            const contentType = parts[0].split(':')[1];
            const raw = window.atob(parts[1]);
            const rawLength = raw.length;
            const uInt8Array = new Uint8Array(rawLength);

            for (let i = 0; i < rawLength; ++i) {
                uInt8Array[i] = raw.charCodeAt(i);
            }

            return new Blob([uInt8Array], { type: contentType });
        }
    </script>
</body>
</html>
```
- `captureArea`のID部分をキャプチャー
- formDateオブジェクトにキャプチャーした部分を`image`というキーで`blobデータ`として追加
- `/save-and-print`というエンドポイントに対して、POSTリクエストを送信


dataURLToBlob関数ではbase64エンコードされたデータURLをBlob形式に変換しています。
dataURLToBlobはパッケージ化されてたのでそちらを使っても問題ないです

https://www.npmjs.com/package/dataurl-to-blob

:::note info
Blobオブジェクトに変換すると、画像ファイルとして保存したり、サーバーにアップロードできます
:::
### サーバ実装

```js:server.js
const express = require('express');
const multer = require('multer');
const { exec } = require('child_process');
const path = require('path');
const fs = require('fs');

const app = express();
const port = 3000;

const upload = multer({ dest: 'uploads/' });

app.use(express.static('public'));

app.post('/save-and-print', upload.single('image'), (req, res) => {
    const tempPath = req.file.path;
    const targetPath = path.join(__dirname, 'uploads/capture.png');

    fs.rename(tempPath, targetPath, err => {
        if (err) {
            console.error(`file_rename_error: ${err}`);
            return res.sendStatus(500);
        }

        exec(`./print_file.sh ${targetPath}`, (error, stdout, stderr) => {
            if (error) {
                console.error(`sh error: ${error}`);
                return res.sendStatus(500);
            }
            console.log(`stdout: ${stdout}`);
            console.log(`stderr: ${stderr}`);
            res.sendStatus(200);
        });
    });
});

app.listen(port, () => {
    console.log(`port:${port}起動`);
});
```
- **Express** を使用してサーバーを設定し、publicフォルダ内の静的ファイルへのアクセスを可能に
- **Multer** を使ってファイルをアップロード
- フロントからのPOSTリクエストを受け取れるエンドポイント`/save-and-print`を設定
- uploadsフォルダ内の`capture.png`へリネーム
- 保存されたファイルをShellScript：`print_file.sh`を実行

### シェルスクリプト

```sh:print_file.sh
#!/bin/bash

file_to_print=$1

if [ -z "$file_to_print" ]; then
    echo "エラー: ファイル指定なし"
    exit 1
fi
if [ ! -f "$file_to_print" ]; then
    echo "エラー: ファイル存在なし"
    exit 1
fi

lpr -P XXXXXXX "$file_to_print"

echo "印刷DONE"
```

- ファイルパスが **指定されてない** or **存在しない** 時のエラーハンドリング
- XXXXXXXの部分でプリンターを指定
- 指定されたファイルを特定のプリンターに送信

※ プリンターの調べ方は下記載せています

### プリンターの調べ方
```terminal:ターミナル
lpstat -s
```
赤線部分をXXXXXXXの部分に指定してあげればOK
![スクリーンショット 2024-08-06 19.08.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616213/7c394acb-825c-b42b-8f6e-01296988b6fe.png)

### その他の機能
`lpr`コマンドには色々指定できるので、サイズなど調整してみてください

![スクリーンショット 2024-08-06 19.56.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616213/fed629a2-7ee2-5b84-bad0-4ef3d5c521d8.png)

### ファイル構成
![スクリーンショット 2024-08-06 19.59.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616213/7e449e29-73c8-70a9-5849-03b2447fa655.png)

### 立ち上げ
```terminal:ターミナル
node server.js
```
`localhost:3000`が立ち上がり、publicフォルダー内の`index.html`を参照してくれます


### 終わりに

これで、ボタンによる自動印刷が可能になると思います！

つまみやGUIなどと組み合わせて、参加者が作った作品を形あるものに出力できたら思い出の一部になりますよね。是非、Webでのグラフィック展示を行う際は、物理的な形で持ち帰る体験を提供してみてはいかがでしょう