【OPOS・POS for .NET】PrintMemoryBitmapで、Graphicsで編集したBitmapを渡せない件

**これは「C#によるPOS開発入門（的な）」と称して連載している記事の1つです。他は[こちら](https://qiita.com/Automatic9045/items/0c8d05e1aa6255f7bd1c)**

# はじめに
`System.Drawing.Bitmap`を渡すとサーマルプリンターで印刷してくれる`PrintMemoryBitmap`メソッドというものがあります。Graphicsを使って生成した画像を渡すことが出来るのは便利ですね。

……と思いきや、なんとこの`PrintMemoryBitmap`メソッド、Graphicsで汚染されたBitmapは受け付けてくれないのです。Graphicsが使えないとこのメソッドの魅力が大幅に減ってしまいます。
そこで、無理やり`PrintMemoryBitmap`メソッドにGraphics由来の画像を突っ込む方法を調べました。

## POS for .NETって？
Microsoftが定めた、POS機器を扱うための共通規格です。機器メーカーにPOS for .NETを通じて動作させるドライバーを作らせることで、使用する機器を他メーカー製のものに入れ替えても今までのコードをそのまま使えるのがポイントです。
POS for .NETによるPOS機器の制御の基礎は[別の記事](https://qiita.com/Automatic9045/items/64073010955ceb8928be)にまとめました。良ければそちらも参考にして下さい。

## 前提コード
以下の全てのサンプルコードの前には、以下のコードがあるものとします。

```C#
/*
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using Microsoft.PointOfService;
*/

Bitmap baseBmp = new Bitmap(200, 150, PixelFormat.Format24bppRgb);
using (Graphics g = Graphics.FromImage(baseBmp)) // 透明なBitmapの全体に赤いバツ印を描く
{
  g.DrawLine(Pens.Red, 0, 0, bitmap.Width, bitmap.Height);
  g.DrawLine(Pens.Red, bitmap.Width, 0, 0, bitmap.Height);
}
```

# まずは普通に渡してみる
```C#
posPrinter.PrintMemoryBitmap(PrinterStation.Receipt, baseBmp, PosPrinter.PrinterBitmapAsIs, PosPrinter.PrinterBitmapRight);
```

これだと例外を吐いて駄目なんですね。

# Bitmapを複製してみる
Bitmapを複製し、新しいものを渡してみます。

```C#
Bitmap newBmp = new Bitmap(baseBmp);
posPrinter.PrintMemoryBitmap(PrinterStation.Receipt, newBmp, PosPrinter.PrinterBitmapAsIs, PosPrinter.PrinterBitmapRight);
```

これでも駄目です。

# （一応OK）一旦ファイルに保存する
`new Bitmap("fileName")`したままのものを渡すと正常に動作するので、一度生成したBitmapをファイルとして保存し、それを読み込み直せば動くのでは？と思いやってみました。

（コードはありません）

勿論これで動くことは動きます。が、一度ファイルにするのは……なんかやだ。パフォーマンスの問題もありますし、何よりスマートじゃない。

# （解決）bmp形式のヘッダーバイナリを自作して、くっつけてみる
`new Bitmap("fileName")`したままのものを渡すと正常に動作することから、元のBitmapからbmp形式のファイルのバイナリを生成し、それを`new Bitmap()`の引数に渡すと上手くいくんじゃないかと予想しました。
そこで、自前でbmp形式のファイルのヘッダーバイナリを作成し、そこに元のBitmapのビットマップ部分をLockBitsで取得したものをくっつけて渡してみます。

```C#
// 32bitアルファチャンネル付bmpを24bitに変換
// EPSON製のプリンターではアルファチャンネルが無視されて真っ黒になるため、アルファチャンネル無しに変換する必要がある
BitmapData bmpData = baseBmp.LockBits(new Rectangle(0, 0, baseBmp.Width, baseBmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

byte[] basePixels = new byte[bmpData.Stride * bmpData.Height];
Marshal.Copy(bmpData.Scan0, basePixels, 0, basePixels.Length);

int stride = (int)Math.Ceiling(3.0 * bmpData.Width / 4) * 4;
byte[] pixels = new byte[14 + 40 + stride * bmpData.Height];
for (int y = 0; y < bmpData.Height; y++)
{
  for (int x = 0; x < bmpData.Width; x++)
  {
    int basePos = 4 * x + bmpData.Stride * y;
    int pos = 14 + 40 + 3 * x + stride * (bmpData.Height - y - 1);

    pixels[pos] = (byte)(255 + (basePixels[basePos] - 255) * basePixels[basePos + 3] / 255);
    pixels[pos + 1] = (byte)(255 + (basePixels[basePos + 1] - 255) * basePixels[basePos + 3] / 255);
    pixels[pos + 2] = (byte)(255 + (basePixels[basePos + 2] - 255) * basePixels[basePos + 3] / 255);
  }
}

// bmpファイルのヘッダーバイナリを自作
byte[] bfSize = BitConverter.GetBytes(pixels.GetLength(0));
byte[] biWidth = BitConverter.GetBytes(bmpData.Width);
byte[] biHeight = BitConverter.GetBytes(bmpData.Height);
byte[] biSizeImage = BitConverter.GetBytes(pixels.GetLength(0) - 14 - 40);

byte[] header = new byte[14 + 40]
{
  0x42, 0x4d, // bfType
  bfSize[0], bfSize[1], bfSize[2], bfSize[3], // bfSize
  0x00, 0x00, // bfReserved1
  0x00, 0x00, // bfReserved2
  0x01, 0x00, 0x00, 0x00, // bfOffBits

  0x28, 0x00, 0x00, 0x00, // biSize
  biWidth[0], biWidth[1], biWidth[2], biWidth[3], // biWidth
  biHeight[0], biHeight[1], biHeight[2], biHeight[3], // biHeight
  0x01, 0x00, // biPlanes
  0x18, 0x00, // biBitCount
  0x00, 0x00, 0x00, 0x00, // biCompression
  biSizeImage[0], biSizeImage[1], biSizeImage[2], biSizeImage[3], // biSizeImage
  0xc4, 0x0e, 0x00, 0x00, // biXPixPerMeter
  0xc4, 0x0e, 0x00, 0x00, // biYPixPerMeter
  0x00, 0x00, 0x00, 0x00, // biCirUsed
  0x00, 0x00, 0x00, 0x00, // biCirImportant
};


Array.Copy(header, 0, pixels, 0, 14 + 40); // ヘッダーとビットマップデータをくっつける
Bitmap newBmp = new Bitmap(new MemoryStream(pixels)); // Graphicsに汚染されていないBitmapの出来上がり
posPrinter.PrintMemoryBitmap(PrinterStation.Receipt, newBmp, PosPrinter.PrinterBitmapAsIs, PosPrinter.PrinterBitmapRight); // これでOK
```

これだと上手くいきました。かなり抜け道的な解法ですが……まあ動いたし、そこまで見た目も汚くないので良いでしょう(?)

# 終わりに
正直私の解決方法も綺麗とは言い難いです。もしもっと良い方法をご存知でしたら教えて頂けると嬉しいです。
元々C++等向けだったのを.NET向けに実装したものなので、この辺りの仕様がこうなっているのは理解出来ますが、やはり使いづらいです。

この記事以外にもPOS開発関連の記事を投稿しています。もし良ければ[こちら](https://qiita.com/Automatic9045/items/0c8d05e1aa6255f7bd1c)からどうぞ。