Web アプリから Bluetooth で印刷する

## Receipt.js

Web Serial API でレシートプリンター (サーマルプリンター) が動きます。  
https://github.com/receiptline/receiptjs  

印刷データはレシートマークダウン。難しいコマンド制御は不要です。  
https://marketplace.visualstudio.com/items?itemName=receiptline.receipt-markdown  

![レシートプリンター](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669512/ae0557d1-bf24-2637-7d69-308b5470e2f2.jpeg)  

## ユーザーの操作

「つなぐ・ひらく・えらぶ」の 3 ステップ。  

### Bluetooth でつなぐ

![つなぐ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669512/89c28712-7e73-a621-d969-774bcbff34a5.png)  
レシートプリンターをペアリングします。  

### Web ページを開く

![ひらく](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669512/c7b94fb7-f06d-81a2-2f25-a9d843da5ff1.png)  
PC 版 Chrome (Edge, Opera) に対応。  

### プリンターを選ぶ

![えらぶ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669512/3bb42734-eee2-b098-353f-91da3c711d51.png)  
プリンター (シリアルポート) を選んで印刷します。  

## 印刷機能の追加

Web アプリ (Web サイト) への追加も簡単。  

### 印刷データを作る

![エディタ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669512/e9bd278e-9c77-f43e-5862-8b198a7bddd5.png)  
[Receipt.js Designer](https://receiptline.github.io/receiptjs-designer/) または [VS Code 拡張機能](https://marketplace.visualstudio.com/items?itemName=receiptline.receipt-markdown) で編集します。  

### HTML に組み込む

```html
<script type="text/javascript" src="script/receipt.js"></script>
<script type="text/javascript" src="script/receipt-printer.js"></script>
<script type="text/javascript" src="script/receipt-serial.js"></script>
<script type="text/javascript" src="script/qrcode-generator/qrcode.js"></script>
```

ライブラリ本体と、オプションのコマンド変換、シリアル通信、二次元コードです。  

### スクリプトを追加する

```js
document.querySelector('button').onclick = () => {
    const markdown = `
    ^^電子チケット控え
    -
    紛失しないよう十分ご注意ください

    {code:NPCCMLIN20230723180001123901;option:qrcode,8,h}

    1階  12列  39番
    `;
    const conn = ReceiptSerial.connect();
    conn.on('ready', async () => {
        await conn.print(markdown);
        conn.close();
    });
};
```

数行の JavaScript で接続して、印刷して、切断。  

## 例のインスタントカメラ

過去記事「[簡単レシート印刷 receiptline と 20 行の JavaScript でレジプリンターをインスタントカメラにしてみた](https://qiita.com/dopperi46/items/1d2b92ba6a4cb6f79c19)」のコードを修正。  

### Chromebook で実行

![インスタントカメラ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669512/fd22f134-1e97-6aae-5100-1df2d6cc44a6.jpeg)  
Chromebook のブラウザだけで完結。Node.js は不要です。  

### 撮影＆プリント

![撮影とプリント](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669512/dc1b74dc-391f-3514-6280-68060192a0d4.jpeg)  
画像をタップして撮影・プリント。接続されていない場合はポートを選択します。  

### ソースコード

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Instant Camera</title>
    <script type="text/javascript" src="script/receipt.js"></script>
    <script type="text/javascript" src="script/receipt-printer.js"></script>
    <script type="text/javascript" src="script/receipt-serial.js"></script>
    <!--<script type="text/javascript" src="script/qrcode-generator/qrcode.js"></script>-->
    <script type="text/javascript">
        async function initialize() {
            const video = document.querySelector('video');
            const canvas = document.querySelector('canvas');
            let conn = null;
            // video
            video.srcObject = await navigator.mediaDevices.getUserMedia({ audio: false, video: true });
            video.onloadedmetadata = event => {
                canvas.height = Math.trunc(canvas.width * video.videoHeight / video.videoWidth);
            };
            video.onclick = event => {
                // image
                canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
                let data = `{i:${canvas.toDataURL('image/png').slice(22)}}\n`;
                // date
                const now = new Date();
                data += `${new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString()}|`;
                // print
                if (conn) {
                    conn.print(data, '-g 1.8');
                }
                else {
                    conn = ReceiptSerial.connect();
                    const timeout = setTimeout(conn.close, 15000);
                    conn.on('disconnect', () => {
                        conn = null;
                    });
                    conn.on('ready', () => {
                        clearTimeout(timeout);
                        conn.print(data, '-g 1.8');
                    });
                }
            };
        }
    </script>
</head>
<body onload="initialize()">
    <video autoplay style="width: 100%;"></video>
    <canvas width="576" height="432" style="display: none;"></canvas>
</body>
</html>
```

以上です。ではまた！