コンテンツにスキップ

Markdown版はこちら

印字ノウハウ・ハマりどころ

プリンター制御仕様 がプロトコルの話であるのに対し、こちらは実際にきれいに印字するための実践的な知見をまとめたページです。ほとんどが実機で試して痛い目を見た結果です。


1. データは「まとめて」流し込む

これが一番重要です。

1bit ラスタ印字 (0x66) は、Command(102, 行数, データ, 1) の 1 回の呼び出しで 行数 × 48 + 5 バイトを 単一の write(2) としてデバイスに流し込みます。ネイティブ側に分割処理は一切ありません。

[0x66][行数 (LE32)][行数 × 48 バイトのラスタデータ]
  1B      4B                    可変長

問題は、この呼び出しを分割すると、その継ぎ目に横線が入ることです。

  ┌────────────────────┐
  │  1回目の Command   │
  │  で送った部分      │
  ├────────────────────┤  ← ここに黒い横スジが入る
  │  2回目の Command   │
  │  で送った部分      │
  └────────────────────┘

対策: 1 枚の縦長ビットマップを 1 回で送る

テキストを 1 行ずつ PrintData() で送るのではなく、印字したい内容全体を 1 枚のビットマップに描画してから 1 回で送出してください。行間のスジが消え、見た目が劇的に良くなります。

レシート全体を Canvas に描いてから流し込む、というのが基本戦略になります。

許容される最大長は未確認

コード上には長さの上限が存在しません。libgzds_utils.so にも CPrint.java にも PrinterServerApp にもチェックはなく、malloc(行数 × 48 + 5) して write() するだけです。実際の上限があるとすれば /dev/prn-dev のカーネルドライバ側ですが、そのドライバのソースは入手できていないため上限は不明です。

実績として分かっているのは以下の数字だけです。

参考値 バイト数 出どころ
PrinterServerApp が実際に発行する最大チャンク 1536 行 × 48 + 5 = 73,733 B chunkHeight = bitmap.width * 4
紙送りバッファ bufferMove のサイズ 100 行 × 48 = 4,800 B CPrint.java の定数
PrintLineFeed() の 1 回あたり 50 行 × 48 = 2,400 B 同上

少なくとも 73KB 程度は一度に通ることが実機で確認できています。どこまで伸ばせるかは未検証なので、長大なレイアウトを扱う場合は自分で試してください。

やむを得ず分割するときは

分割の継ぎ目を減らすため、チャンクはできるだけ大きく取ります。また、次のチャンクを投げる前にプリンターのビジー状態が抜けるのを待ちます。

while (CPrint.getPrinterStatus() == 1) {
    Thread.sleep(50)
}

2. グレースケールモードは印字幅が狭い

プリンターには 1bit モノクロのほかに 16階調グレースケールモード (0x67) があります。ただし印字幅が 384px → 240px に狭くなります。

1bit モノクロ グレースケール
印字幅 384 px 240 px
1行あたり 48 バイト 120 バイト
階調 2 16
送信単位 任意行数をまとめて 1 回 1 スキャンラインずつ固定

元アプリでの用途は測定者の顔写真の印刷のみ(240×240 にリサイズして印字)で、おそらくこのモードは顔写真の印刷のために用意されたものです。

グレースケールは 1 ライン = 1 コマンド固定

0x67 パスは libgzds_utils.so の中でペイロード長 120 バイトがハードコードされており、param1 は無視されます。つまり「まとめて流し込む」ことが構造的にできず、240 行の画像なら 240 回のコマンド発行になります。 幅の狭さと合わせて、テキストや罫線を含むレイアウト全体は 1bit モードで組むのが基本です。

使い分けの目安

印字したいもの 推奨モード
テキスト、罫線、QR コード、ロゴ 1bit (384px) — 幅が広くエッジが立つ
写真・グラデーション グレースケール (240px)、または 1bit + ディザリング

3. 1bit で写真を印字するなら前処理が効く

1bit モードで写真を印字する場合、単純な閾値 2 値化では潰れます。PrinterServerApp では以下を実装しています。

手法 向き・特徴
単純2値化 (Threshold) 線画・文字・ロゴ。輪郭がくっきり出る
ベイヤー (Bayer) 規則的なパターン。グラデーションが安定するが模様が目立つ
フロイド–スタインバーグ 写真向き。最も自然だが、感熱紙では細かいドットが飛びやすい

輪郭抽出ブレンドを噛ませる

ディザリングだけだと輪郭がぼやけます。3×3 ラプラシアンでエッジを抽出し、Weight × エッジ画像 + (1 - Weight) × 元画像 でブレンドしてから 2 値化すると、境界線に確実に黒ドットが落ちてシャープになります。

黒レベル・白レベル・ガンマを広めに振る

PrinterServerApp では Black / White Level の範囲を標準の 0〜100 から -50〜150 に、ガンマを最大 5.0 まで拡張しています。感熱紙は中間調の再現域が狭いので、元画像のコントラストを事前に大きく潰しておく方が結果が良くなります。


4. 電圧降下によるかすれ

安価なサーマルヘッド共通の物理特性として、1 ラインあたりに同時加熱するドットが多いと電源電圧が落ちて印字がかすれます。

ベタ塗りの大きな黒面を印字すると、その部分だけ薄くなるのはこのためです。

対策

  • ベタ塗りを避け、ディザやハーフトーンで黒ドット密度を落とす
  • 大きな塗りつぶし矩形は、白抜き文字+細めの枠線に置き換える

PrinterServerApp のエディタでは、1 ラインあたりの黒画素率が 30% を超えると、超過分に応じてプレビュー上の黒を薄く描画して、この現象を事前に可視化しています(印刷データ生成時はバイパスされます)。


5. 濃度設定 (PrinterSetDarkness) は効いていない

PrinterSetDarkness(byte darkness) は API として存在しますが、現在の呼び出し方では濃度値がプリンターに届いていません。

printerType = 1 のパケット組み立てはデータ長を param1 × 48 で計算するため、param1 = 0 で呼ばれているこのコマンドではペイロードが 0 バイトになります。詳細は プリンター制御仕様 §3 を参照してください。

濃度を変えたい場合は、当面画像側(ガンマ・黒レベル)で調整するのが現実的です。


6. 用紙まわり

  • 紙幅 57〜58mm、ロール外径 25mm まで。市販のレシート用紙がそのまま使えます
  • PrintCheckPaper() (0x77) は用紙有無を見ますが、getPrinterStatus() (0x44) は 紙の有無では変化しません(0 = 待機 / 1 = 印刷中のみ)
  • 印字後は PrintLineFeed(3) 程度送ってからカットすると、最終行が切り取り線に隠れません

関連ページ