小さなブログ

レシート・グラフィックス

(2025.05 更新)

レシートプリンターで画像をきれいに印刷する方法を模索します。

印刷の基本

レシートプリンターとは

お店のレジで細長い紙の簡易領収書、いわゆるレシートを渡されますね。あれの印刷機器がレシートプリンターです。感熱ロール紙に低解像度のモノクロ(白黒二値)印刷をするのが特徴。より一般的には「サーマルプリンター」といい、FAXにも使われています。

サーマルプリンター(Wikipedia)

組み込み用途のB2B製品と思われがちですが、意外なことに一般消費者向けにも販売しており、Windowsなどから汎用プリンターとして使えます。ただし、幅が極端に狭く解像度も極めて低いため、ふつうの文書を印刷してもろくな結果になりません。

AMとFM

それでも、画像を単体で印刷するとそれなりに見栄えがします。ドライバーが気を利かせて、うまいこと白黒二値に減色処理してくれるのです。Windowsの印刷設定ダイアログでは、その処理方法を選ぶこともできます(昔はなかった気がする?)。

フルカラーを単色やCMYKといったわずかな原色に分解する減色処理は、印刷の品質を左右する重要な要素です。そのわずかな原色で濃淡や中間色を表現する方法には、大きく分けて「AM」と「FM」の二種類あります。AM(振幅変調)は、濃度を「ドットの大小」で表現する方法、いわゆる網点・ハーフトーン。FM(周波数変調)は、濃度を「ドットの出現頻度」で表現する方法。デフォルトはAMだったりしますが、レシートプリンターの特性に合うのはFMのほうでしょう。

このFM減色処理を実現するのがディザリングと呼ばれる手法で、多くのアルゴリズムが考案されています。

ディザ(Wikipedia)

しかし、印刷ダイアログではそこまで細かな指定はできません。これでは気持ち悪いため、画像をあらかじめ自前で減色処理してやることにします。

誤差拡散法

ディザリングのアルゴリズムにはいくつかの系統があります。最も素材に忠実なのは「誤差拡散法」。減色によって余った情報を、ただ切り捨てるのではなく近傍ピクセルに散らす(加える)ため、全体として失われる情報が少なく済むのです。

情報の効果的な散らし方は多く研究されています。中でも古典的かつ代表的なのが「フロイドとスタインバーグ」によるもの。見栄えが良く、処理効率も悪くない。以下これを採用して話を進めます。

フロイド-スタインバーグ・ディザリング(Wikipedia)

ImageMagickによる実践

誤差拡散の実装は、画像処理プログラミングに慣れていれば難しくありません。しかし、代表的なアルゴリズムなら大抵の各種画像処理ソフトウェアにすでに備わっており、それらを利用するほうがはるかに簡単。次のコマンドは「ImageMagick」を使う方法です(インストール方法は適当に調べてください)。

$ convert input.jpg -resize 384x -dither FloydSteinberg -remap pattern:gray50 output.png

「convert」はImageMagickのCLIコマンド(環境によっては「magick」かも)。オプションの「-resize 384x」で入力画像をレシートプリンターの解像度にリサイズし(ここでは横384ドット)、「-dither FloydSteinberg」でフロイドとスタインバーグのディザリング、「-remap pattern:gray50」で白黒二値化を指示します。

Command-line Options(ImageMagick)

なお、ImageMagickで利用できるディザリングのアルゴリズムは次のコマンドで列挙できます。

$ covnvert -list dither

ESC/POSによる印刷

汎用プリンターとしての問題

前述のとおり、レシートプリンターは適切なドライバーさえあればPCから汎用プリンターとして扱えます。共有などもできて便利な一方、この仕組みには次のような問題もあります。

これらを克服するために使いたいのが「ESC/POS」です。というより、これなしではレシートプリンターは単なる低画質プリンターにすぎません。ESC/POSによる制御は事実上必須といえるでしょう。

ESC/POSとは

ドットインパクト方式などのラインプリンター(タイプライターのごとく行単位で文字列を印字するプリンター)が主流だったころ、その制御は主にASCII制御コード(たとえば「改行」のように、目に見える字形を持たず機能だけを持つ“文字”)で行っていました。やがてそれらだけでは足りなくなると、ESCから始まる制御コードシーケンス仕様「ESC/P」をエプソンが定めて普及しました。「ESC/POS」はそのレシートプリンター版で、メーカー問わず多くの機器が準拠しています。

プリンターに文字列を与えるには標準入出力を用います。対象のプリンターを表すデバイスファイルに、制御コードの文字列をパイプやリダイレクトで入力してやるのです(あるいはプログラムからデバイスファイルを開いて書き込む)。ユーザーフレンドリーとは言い難いものの、単純明快で技術者フレンドリーではあります。プリンターがデバイスファイルとして認識されれば十分なため、ドライバーの問題が少ないのも利点です。

画像印刷コマンド

ESC/POSによる印刷を試してみましょう。以下、うちにあるレシートプリンター「SANEI BL2-58U」を例にします。そのコマンドリファレンスをメーカーサイトからダウンロードしました。

マニュアル(三栄電機)

どうやら「DC2 V:ラスタビットイメージの印字」コマンドで画像を印刷できるようです。さっそく実装してみます。

画像を読み込んでピクセルを走査でき、文字列の標準出力さえできれば処理系は何でもかまいません。ここではお手軽にCLI上のPHPで(要GDモジュール)。例示のためエラー処理を端折りつつ、処理がわかりやすいよう素直にコーディングしました。ESC/POSを理解するには、「echo」で標準出力しているところだけ読めば十分です。

<?php

// ESC/POSによる画像印刷関数(「DC2 V」コマンドを標準出力)
function writeImage(
  $image, // GD画像オブジェクト(あらかじめ印刷解像度へのリサイズ・白黒ディザ化を推奨)
  $lineWidth = 384 // プリンターの横解像度(機種により固定)
) {
  // 画像の幅と高さを得る
  $width = imagesx($image);
  $height = imagesy($image);

  // 画像サイズを印刷解像度以内に丸め、白黒にする
  if ($width > $lineWidth) $image = imagescale($image, $lineWidth);
  imagefilter($image, IMG_FILTER_GRAYSCALE);
  imagefilter($image, IMG_FILTER_CONTRAST, -1000);

  // 印刷コマンド出力開始
  echo "\x12V"; // DC2 V ラスタービットイメージコマンド
  echo chr($height & 0xff) . chr(($height >> 8) & 0xff); // 縦ピクセル数(16ビット・リトルエンディアン)

  // 画像を横8ピクセルずつ読み、ビット列として出力
  for ($y = 0; $y < $height; $y++) {
    for ($x = 0; $x < $lineWidth; $x += 8) {
      for ($i = 0, $bits = 0; $i < 8; $i++) {
        if (@imagecolorat($image, $x + $i, $y) === 0) $bits |= 0x80 >> $i;
      }
      echo chr($bits);
    }
  }
}

// 使用例:引数で渡されたPNGファイルをロードして関数実行
$image = imagecreatefrompng($argv[1]);
if ($image) writeImage($image);

このコードをたとえば「writeImage.php」というファイル名で保存し、次のようなコマンドで実行します。

# php writeImage.php image.png > /dev/usb/lp0

引数の「image.png」は印刷したい画像です(このコード例ではPNGファイルしか受け付けない)。あらかじめ解像度をレシートプリンターに合わせ、先のImageMagickの例のように白黒二値にディザリングした画像を指定します。

標準出力リダイレクト先の「/dev/usb/lp0」がレシートプリンターのデバイスファイルです。ただし、この名前は環境により異なるので注意。うちのLinux環境では「USB接続のラインプリンター0番」としてこのように現れました。アクセスにroot権限を要したりもしますが、あくまでも例として。

実行すると、画像が驚くほど即座に印刷されます。生のコマンドを送っているため、マージンのための紙送りといった余計な動作も生じません。

もうひとつのコマンド

リファレンスによると、先の「DC2 V」コマンドは確かにESC/POSの一部らしいのですが、だからといってすべてのESC/POS準拠プリンターで実行できるとは限りません。準拠といっても、全コマンドを実装する義務はないようですから(?)。

ネットでちょっと調べてみると、前のコマンドよりも「ESC *:ビットイメージ指定」コマンドのほうが対応している機種が多い模様。よって、そちらのコマンドでの印刷処理も実装してみました。

<?php

define('BITIMAGE_MODE', 33); // ビットイメージモード:33(24ドット倍密度)固定
define('LINE_HEIGHT', 24); // 行の高さ:24ドット固定

// ESC/POSによる画像印刷関数(「ESC *」コマンドを標準出力)
function writeImage2(
  $image, // GD画像オブジェクト(あらかじめ印刷解像度へのリサイズ・白黒ディザ化を推奨)
  $lineWidth = 384 // プリンターの横解像度(機種により固定)
) {
  // 画像の幅と高さを得る
  $width = imagesx($image);
  $height = imagesy($image);

  // 画像サイズを印刷解像度以内に丸め、切りを良くする
  if ($width > $lineWidth) {
    imagescale($image, $lineWidth); // 画像の幅を印刷行の幅に合わせる
    $width = $lineWidth;
  }
  $width = (int)(($width + 8 - 1) / 8) * 8; // 画像の幅を8ドット単位に丸める
  $height = (int)(($height + LINE_HEIGHT - 1) / LINE_HEIGHT) * LINE_HEIGHT; // 画像の高さを行ドット単位に丸める

  // 画像を白黒にする
  imagefilter($image, IMG_FILTER_GRAYSCALE);
  imagefilter($image, IMG_FILTER_CONTRAST, -1000);

  echo "\x0d"; // 行頭復帰
  echo "\e\x33" . chr(LINE_HEIGHT); // 隙間が空かないよう改行の高さを印刷行の高さに合わせる

  // 印刷コマンド出力開始
  for ($y = 0; $y < $height; $y += LINE_HEIGHT) {
    echo "\e*" . chr(BITIMAGE_MODE) . chr($lineWidth & 0xff) . chr(($lineWidth >> 8) & 0xff); // ESC * ビットイメージコマンド(毎行出力)

    // 画像を縦8ピクセル✕3(1行24ピクセル)ずつ読み、ビット列として出力
    for ($x = 0; $x < $width; $x++) {
      for ($i = 0; $i < LINE_HEIGHT; $i += 8) {
        for ($j = 0, $bits = 0; $j < 8; $j++) {
          if (@imagecolorat($image, $x, $y + $i + $j) === 0) $bits |= 0x80 >> $j;
        }
        echo chr($bits);
      }
    }
  }

  echo "\n"; // 最後に改行
  echo "\e\x32"; // 改行の高さを初期化
}


// 使用例:引数で渡されたPNGファイルをロードして関数実行
$image = imagecreatefrompng($argv[1]);
if ($image) writeImage2($image);

しかし、こちらのコマンドには少々気になる短所があります。画像を行単位で印刷する仕組みのためか、行の境目がかすかに見えてしまうのです。隙間が空くわけではなく、微妙な濃淡の差が生じます。機種によっては目立たないかもしれませんが、プリンターさえ対応していれば「DC2 V」コマンドのほうを勧めます。

余談

レシート紙やFAX紙などのサーマルプリンター用感熱紙は、化学的な表面加工が施されているためリサイクルできません(熱で黒くなる性質は単純に考えてまずいですね)。混ざってしまうと他の紙までリサイクルできなくなるので、資源ゴミに出さないでください。

ちなみに、感熱紙は研磨紙としても使えます。表面加工の微細な粒子がヤスリとして働くためで、「レシートは爪磨きになる」との雑学として知られています。副作用があるかもしれず自己責任ですが、わたしは金属端子の曇り取りなどに使っています。

#note