【誰得】普段目にする、レシート印刷の実装方法


#なぜこの記事？
自分は幸運にも、一人で伝票システムアプリを作る機会が得られました！
（それについての記事はこちら:
 [八丈島のホテルで、運用費用０円の伝票システムアプリ作って、業務改善した話。](https://qiita.com/Yoji0806/items/d895d1fbd2d8c1736449)）

さらにその中で普段（少なくとも個人開発）では学ぶことがないであろう**レシートを作る実装**を学べました。

その時はGoogleで調べてももちろん参考にする記事など無く（あるのかもだけど）、**プリンターのドキュメント**を頼りに進めていきました。

なのでこの記事でまとめておきます。
今後レシート印刷を実装する人にとって、この記事が開発の**高速道路**を進む手助けになれば幸いです。

また、この部分を知ることで普段受け取るレシートに目が行くようになり、**日常の見え方が変わった**ので知識として知っておくのも良いかもしれません:nerd:


（※自分の体験を基にしたので、正解としてではなく、参考程度に読んでください！）


#実装の流れ

- (プリンターを選ぶ)
- （レイアウトの構想）
- レシートデータ作成関数をつくる
    - レイアウトを整える
    - builderにバイトデータを格納
    - プリンターのportを開ける
    - 初期化


#プリンターを選ぶ

この段階では、自分の**目的を明確にする**必要があります。
具体的には、

- **何を** （レシートか、写真か、クーポンか..）
- **どうやって**（通信規格:Bluetooth かイーサネットか..）
- **どのように** （欲しい機能や性能）
- **プラットフォーム** （webかandroidかiOSか..）

です。
そして必要な機能を絞ったら後は値段とパフォーマンスとの天秤にかけます。

自分の場合は、

- 何を --- **会計レシートを**
- どのように --- **速く**
- どうやって --- **Bluetoothで**
- プラットフォーム --- **Android上で**
でした！

これにプラスで、自分は**ドキュメントやサンプルが豊か**かどうかも考慮に入れました。

その結果、プリンターは**StarPrntのTSP650ii**を購入しました。
製作しているのは、スター精密という自動車や時計の精密機器を扱っている日本の会社で、プリンター界(?)でも老舗だと思います！信頼性抜群！
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266096/8d359aaf-8b37-16d5-b4dd-8885f47594f5.png):[公式サイト](https://www.star-m.jp/products/s_print/sp01_08.html)より

決め手は以下👇のような点です！

>- iOSモードと標準モード(**Android**/Windows等)に対応
- **Bluetoothインターフェースモデル**
- スター**最速印字**が可能(最大300㎜/s)
- StarIO(通信サポートソフト)を利用すれば、iOS、Androidなど多様なOS上でのアプリケーション開発における**工数大幅削減**が可能
- **SDK**が多機能で使いやすそう。ドキュメントも丁寧



個人でなにかプリンター接続で遊びたい、などの目的では安いモデル（3~5000円程）もありましたが、大抵はWindowsのみ対応だったり、専用のモバイルアプリのみ対応だったりします。

自分でプリンターを組み込んでアプリを作る場合はお金が許すのならスター精密が一番だと思います！（設定・接続もわや楽）


また、プリンターの種類は勿論、インクを使用する機器ではなく、熱を使うサーマルプリンターの方が圧倒的に楽で良いと思います。

ここからは、スター精密のSDKを使っている前提で話していきます。
👇
👇


#レイアウトの構想
次にレイアウトの構想ですが、最初の段階で**自由にレイアウトを描いてはいけない**のがコツです。
では最初になにをやるというと、

1. 横１行に入る最大**全角文字**数を測る。
- レシートに記載する必要情報をまとめる。

です！（②の説明は省略）
①でなぜ全角なのかというと、これまで自分は知らなかったのですが、
**半角は全角の半分ではない**のです！

詳しく言うとディスプレイに表示される文字には大きく２種類、
``等幅フォント``と、``プロポーショナルフォント``があります。


``等幅フォント``は原稿用紙のマス目みたいに横幅が決まっており、**半角文字は全角文字の半分の幅**になっています。

対して、``プロポーショナルフォント``は、文字一つ一つが綺麗にみえるために**文字の大きさによって幅が変わって**きます。

><img width="314" alt="2019-06-18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266096/eeb0efdf-279a-daf5-8521-15c96e09f742.png">
: 参考[【「全角」と「半角」、「等幅」と「プロポーショナル」】](https://www.724685.com/word/wd110302.htm)


印刷物などでは``プロポーショナルフォント``が採用されていることが多く、TSP650も例外ではありません。


なので、固定されているレイアウトは良いのですが、注文名などの文字数が可変的な情報は**全ての文字を全角 or 半角 で統一する必要があります。**
（レイアウトを整えるという点で）

👇例えば、紙の横１行が全角で24文字入り、最後に何文字か残したい場合は、こうします。（自分の例では数量を入れたかったので）

```kotlin
  //注文名をすべて全角に
  //１行に収まるように調節
       var order = Transliterator.getInstance("Halfwidth-Fullwidth").transliterate(orderList[x])
       if (order.count() >= 18){
            order = order.substring(0, 18)
        }

        val size = order.count()
        val rest = 22 - size
        data = (order + "　".repeat(rest - 3) + qList[x] + "\n")    //行数合わせのコード
```
(変数orderには注文名が入っている。)

**半角→全角**（全角→半角）の変換はライブラリ「[ICU](http://site.icu-project.org/)」を使えば１行で変換できます！
(参考: [Javaにおける文字列の全角⇔半角変換について](https://qiita.com/makimaki913/items/df745b85b802099a6e32))




ここまで来たら紙でも何でも使って、自由にデザインを決められます。
最初から紙を使うのは、**自由度が高すぎ**るので、**出来ること**・**必要なこと**を蔑ろにして、楽しくお絵かきをしてしまう危険性があります（笑）

自分はここの段階では、コードを変えて色々レイアウトを試しました。
そしてユーザー（自分の場合、ホテルスタッフや支配人）からのフィードバックを得て、最適なレイアウトにどんどん近づいていきました！

👇最初はこんな感じでひどかったのですが...
![ooooi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266096/41e2b416-14b8-979c-a108-f3ba53c2c5ac.png)


👇最終的にはこのレイアウトに行きつきました。
![iei.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266096/936078f0-cacf-1e75-dedd-ecae3c39a91f.png)

文字の大きさは、**縦(横)だけn倍**・**縦横n倍**を使って分かりやすく、また**文字の太さ**や**中央寄せ**も利用して見やすいレイアウトを心がけました！

自分は**シンプル&見やすさ**を一番に考えたのですが、スター精密のプリンターは本当に多機能で、ロゴを入れたり、QRコードやバーコードももちろんつけれたりで、自作でもお洒落なレシートも簡単に作れそうです。


#レシートデータ作成関数をつくる

ここからが主にコードの問題になります。
自分は手をつける前**ここが鬼門**だと思っていました...。
しかしいざドキュメントを見ると、、、

<img width="960" alt="2019-06-21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266096/9a6f804e-deea-626e-c0a9-fe6851d6c4c5.png">

> 引用: [公式ドキュメント](http://sp-support.star-m.jp/SDKDocumentation.aspx)より

**(;''∀'')めっちゃ簡単そう!!!**
実際簡単でした！

自分で実装しないといけない箇所は
①**レイアウトをコード内に記し**て、
②**byte型に変換してbuilderにデータを入れる**、
の２つです。
（上記の画像では変数**command**がbuilderになっています）


##レイアウトを整えるコード

まず上に貼った、自分が最終的に作ったレシートレイアウトを作るコードを貼っておきます。
（文字数が可変的なものは、変数に格納されており、すでに**全角&適切な文字数**になっている）
実際のレシートと見比べるとわかりやすいと思います！

```kotlin:レシートレイアウト
 //レシート情報の作成
   val time = getToday()

   val content0 =
            ("レストラン タルタルーガ\n").toByteArray(charset("SJIS"))
   val content1 =
            ("--$time--\n\n").toByteArray(charset("SJIS"))
   val content2 =
            ("お会計はチェックアウトの際にお願いします。" + "\n" + "\n" + "\n").toByteArray(charset("SJIS"))
   val content3 =
            ( "テーブル番号   $tableNum\n" +
                                "お部屋番号      $room\n").toByteArray(charset("SJIS"))
   val content4 =
         ("\n【$tablet】\n注文内容    (@単価)　　　　　　　　　数量\n"
                                + "------------------------------------------------\n").toByteArray(charset("SJIS"))

//5.6は下でmake_order()を呼び出して作っている。

    val content8 =
          ("      合計    \\ $total\n").toByteArray(charset("SJIS"))
    val content9 =
          ("  --サインはこちらへ--\n" + "\n" + "\n" + "\n" + "\n").toByteArray(charset("SJIS"))

```

(👆空白はスペース、改行は "\n" で表されています。あとなぜかSJISに一回しないと通りませんでした。)


最初の`getToday()`はおなじみのこのコードです👇

```kotlin
   fun getToday(): String {
        val date = Date()
        val format = SimpleDateFormat("yyyy/MM/dd/ HH:mm", Locale.getDefault())
        return format.format(date)
    }
```




##builderにバイトデータを格納

あとは、builder()にデータを入れていきます

```kotlin
   val builder = StarIoExt.createCommandBuilder(StarIoExt.Emulation.StarLine)
   builder.beginDocument()
   builder.appendEmphasis(true)

     //write content0
                
   builder.appendAlignment(ICommandBuilder.AlignmentPosition.Center)
   builder.appendMultipleHeight(content0, 2)

                //1
   builder.appendEmphasis(false)
                
   builder.appendAlignment(ICommandBuilder.AlignmentPosition.Center)      
   builder.append(content1)

                //2
   builder.appendEmphasis(true)
   builder.appendMultipleHeight(content2, 2)

                //3
                
   builder.appendAlignment(ICommandBuilder.AlignmentPosition.Left)
   builder.appendMultiple(content3, 2, 2)
                //4
   builder.append(content4)
　　　・・・続く
```
👆この段階で文字の大きさ・太さ・中央寄せなどを記します。


ちなみに自分が作った伝票システムでは、**注文ごとをブロック(線)で区切る**必要があり,以下のコードを書きました。（複雑なので説明はカットします**(ﾉД`)**）

```kotlin
    //ブロック毎に("------")をつけるためのコード
  if (x + 1 == inBlockList[y] + z){
        z += inBlockList[y]
        val a =   ( "------------------------------------------------").toByteArray(charset("SJIS"))
        builder.append(a)
         y += 1
   }
```
このようなコードも全て自分で書く必要があります。
（最初は全部SDKが自動でやってくれるものかと...）



そして最後に**紙を切る**処理を入れて、builderに入っているデータをまとめます。


```kotlin                
   builder.appendCutPaper(ICommandBuilder.CutPaperAction.PartialCutWithFeed)
   builder.endDocument()

   val cmd = builder.commands
```




##プリンターのPortをopen

あとは、プリンターのPortをopenして、ドキュメントの通りにデータをプリンターに送ります。

```kotlin
 //プリンターのportオープン
  var port: StarIOPort? = null

  try {
        port = StarIOPort.getPort("BT:", ";d3", 3000)
        var status = port.beginCheckedBlock(
        port.writePort(cmd, 0, cmd.size)
        status = port.endCheckedBlock()

        if (!status.offline) {
           // 印刷正常終了（プリンタオンライン）
            toast("印刷が完了しました。")
        } else {
           // 印刷異常終了（紙無し、プリンタカバーオープンなど）
           // ユーザーに通知
        }
  } catch (e: StarIOPortException) {
          toast("$e")
          Log.e("startPortのエラー", "$e")
         // エラー発生
  } finally {
            try {
                // ポートクローズ
               StarIOPort.releasePort(port)
             } catch (e: StarIOPortException) {
                   toast("エラー２: $e")
                 }
   }
```
(※エラー部分は適時変えておいてください。)


##そして、初期化

最後にこの``レシート情報作成関数``で使用したリストなどを初期化しましょう！
初歩的なことですが大事です。
自分はこれを忘れていて何分か無駄にしました**(';')**


#おまけ
レシート作成は自分で書いたコードが画面上ではなく、**リアルに反映**されるのでなかなか楽しかったです。
レイアウトを考える作業なんかも職人気分で没頭して、**10時間ぶっとうし**で作りました。

通してわかったコツは、とにかく**試行錯誤の回数**だと思います。
レシート作成に限らず、ここの泥臭さを楽しめたら一気に進むと思います！
　　　　（80Lぐらいのゴミ袋がパンパンになったの写真👇）
![123.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266096/96ce8f64-dd7f-42c4-d6bb-9b75f276a562.png)


最後までありがとうございました！