TwainDotNetが内部で32ビットBitmapを作成する部分を24ビット以下にも対応

#1. はじめに

[TwainDotNet](https://github.com/tmyroadctfig/twaindotnet)でグレースケールをスキャンすると、内部で32ビットBitmapが生成され、保存した時にファイルサイズが大きくなります。このため、グレースケールの場合には8ビットで保存できるようにTwainDotNetを改造してみました。1ビット、24ビットにも対応しています。[スキャンアプリケーション ITScan - Qiita](https://qiita.com/mayo00/items/01a22bbe1e9283fce6cb)に含めています。

#2. 変更前ソース

TwainDotNetバージョン1.0.0を元にしています。
BitmapコンストラクタにPixelFormatの指定がなく、必ず32ビットのBitmapが作成されるようになっています。

```cs:BitmapRenderer.cs抜粋
        public Bitmap RenderToBitmap()
        {
            Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

            using (Graphics graphics = Graphics.FromImage(bitmap))
            {
                IntPtr hdc = graphics.GetHdc();

                try
                {
                    Gdi32Native.SetDIBitsToDevice(hdc, 0, 0, _rectangle.Width, _rectangle.Height,
                        0, 0, 0, _rectangle.Height, _pixelInfoPointer, _bitmapPointer, 0);
                }
                finally
                {
                    graphics.ReleaseHdc(hdc);
                }
            }

            bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

            return bitmap;
        }
```

#3. 変更後ソース

元データが32ビット以外の場合にビット数に合わせたビットマップを構築しています。詳細は後述します。32ビットの場合は元の処理を行っています。32ビットの場合も新しい処理で大丈夫かもしれませんが、TWAIN機器から32ビットのデータが来るケースのテストができないため、安全のために元の処理にしています。

```cs:BitmapRenderer.cs抜粋
        public Bitmap RenderToBitmap()
        {
            if (_bitmapInfo.BitCount != 32)
            {
                int sizeBitmapFileHeader = Marshal.SizeOf(typeof(BitmapFileHeader));

                BitmapFileHeader bitmapFile = new BitmapFileHeader();
                bitmapFile.Type = 'M' * 256 + 'B';
                bitmapFile.Size = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader + _bitmapInfo.SizeImage;
                bitmapFile.Reserved1 = 0;
                bitmapFile.Reserved2 = 0;
                bitmapFile.OffBits = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader;

                IntPtr _bitmapFilePointer = Marshal.AllocHGlobal(sizeBitmapFileHeader);
                Marshal.StructureToPtr(bitmapFile, _bitmapFilePointer, true);

                byte[] buffer = new byte[bitmapFile.Size];
                Marshal.Copy(_bitmapFilePointer, buffer, 0, sizeBitmapFileHeader);
                Marshal.Copy(_bitmapPointer, buffer, sizeBitmapFileHeader, bitmapFile.Size - sizeBitmapFileHeader);

                Bitmap bitmap = null;
                using (MemoryStream ms = new MemoryStream(buffer))
                using (Bitmap bmp = new Bitmap(ms))
                {
                    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                    try
                    {
                        bitmap = new Bitmap(bmp.Width, bmp.Height, bmp.PixelFormat);
                        BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                        try
                        {
                            int bmpBufferSize = bmpData.Stride * bmp.Height;
                            byte[] bmpBuffer = new byte[bmpBufferSize];
                            Marshal.Copy(bmpData.Scan0, bmpBuffer, 0, bmpBufferSize);
                            Marshal.Copy(bmpBuffer, 0, bitmapData.Scan0, bmpBufferSize);
                        }
                        finally
                        {
                            bitmap.UnlockBits(bitmapData);
                        }
                    }
                    finally
                    {
                        bmp.UnlockBits(bmpData);
                    }

                    if (_bitmapInfo.BitCount < 24)
                    {
                        bitmap.Palette = bmp.Palette;
                    }
                }

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;
            }
            else
            {
                Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);

                using (Graphics graphics = Graphics.FromImage(bitmap))
                {
                    IntPtr hdc = graphics.GetHdc();

                    try
                    {
                        Gdi32Native.SetDIBitsToDevice(hdc, 0, 0, _rectangle.Width, _rectangle.Height,
                            0, 0, 0, _rectangle.Height, _pixelInfoPointer, _bitmapPointer, 0);
                    }
                    finally
                    {
                        graphics.ReleaseHdc(hdc);
                    }
                }

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;
            }
        }
```

既存のBitmapInfoHeader.csを元に、下記のファイルを同じように作成しました。

```BitmapFileHeader.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Diagnostics;

namespace TwainDotNet.Win32
{
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    public class BitmapFileHeader
    {
        public short Type;
        public int Size;
        public short Reserved1;
        public short Reserved2;
        public int OffBits;

        public override string ToString()
        {
            return string.Format(
                "t:{0} s:{1} r1:{2} r2:{3} o:{4}",
                Type,
                Size,
                Reserved1,
                Reserved2,
                OffBits);
        }
    }
}
```

#4. 解説

変更前は下記のようにBitmapコンストラクタにPixelFormatの指定がなく、必ず32ビットのBitmapが作成されるようになっています。

```cs
            Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);
```

ここの前に、ビット数が32ビット以外の場合に分岐を加えます。24ビット以下向けの対応ですが、64ビットの場合にもここへ来ます。(64ビットビットマップを見たことありませんが)

```cs
            if (_bitmapInfo.BitCount != 32)
            {
```

但し、BitmapコンストラクタにPixelFormat.Format8bppIndexedを指定してGraphics.FromImage()を行うと例外が発生してGraphicsオブジェクトを作成できません。
このため、GraphicsオブジェクトからgetHdc()を行ってhDCを取得し、SetDIBitsToDevice()を行うという手が使用できません。

```cs:失敗例
            if (_bitmapInfo.BitCount != 32)
            {
                Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height, PixelFormat.Format8bppIndexed);

                using (Graphics graphics = Graphics.FromImage(bitmap))
                {
```

これを回避するため、MemoryStreamからBitmapを作成しています。但し、元データにBitmapFileHeaderがないため、自前で作成しています。
BitmapFileHeader作成、byte配列の先頭にコピーします。

```cs
                int sizeBitmapFileHeader = Marshal.SizeOf(typeof(BitmapFileHeader));

                BitmapFileHeader bitmapFile = new BitmapFileHeader();
                bitmapFile.Type = 'M' * 256 + 'B';
                bitmapFile.Size = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader + _bitmapInfo.SizeImage;
                bitmapFile.Reserved1 = 0;
                bitmapFile.Reserved2 = 0;
                bitmapFile.OffBits = (_pixelInfoPointer.ToInt32() - _bitmapPointer.ToInt32()) + sizeBitmapFileHeader;

                IntPtr _bitmapFilePointer = Marshal.AllocHGlobal(sizeBitmapFileHeader);
                Marshal.StructureToPtr(bitmapFile, _bitmapFilePointer, true);
```

BitmapInfoHeaderとカラーテーブル(ある場合のみ)とビットイメージをBitmapFileHeaderの次にコピーします。

```cs
                byte[] buffer = new byte[bitmapFile.Size];
                Marshal.Copy(_bitmapFilePointer, buffer, 0, sizeBitmapFileHeader);
                Marshal.Copy(_bitmapPointer, buffer, sizeBitmapFileHeader, bitmapFile.Size - sizeBitmapFileHeader);
```

MemoryStreamを作成し、そこからBitmapを作成します。それを呼び出し元で保存すると、グレースケールの場合には8ビットのBMPやPNG等で保存できます。
但し、これだけだとMemoryStreamを解放していないのでメモリリークします。

```cs:失敗例1：MemoryStreamを解放していない
                MemoryStream ms = new MemoryStream(buffer);
                Bitmap bitmap = new Bitmap(ms);

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;
```

MemoryStreamをusing等で解放すると、画像の保存時(Image.Saveメソッド実行時)に「GDI+で汎用エラーが発生しました」が発生します。

```cs:失敗例2：呼び出し元でSave時に「GDI+で汎用エラー」
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bitmap = new Bitmap(ms);

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }
```

これを回避するため、Bitmapを元にBitmapオブジェクトを作り直してそれを返却しています。但し、単純に Bitmap bitmap = new Bitmap(bmp) とすると、32ビットBitmapになってしまいます。

```cs:失敗例3：32ビットBitmapになる
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);
                    Bitmap bitmap = new Bitmap(bmp);

                    bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                    return bitmap;
                }
```

この回避のために、LockBits()をして、PixelFomatかつビットマップイメージの引数があるコンストラクタを呼んでいます。それだけだとカラーテーブルがコピーされないため、24ビット未満の場合にはカラーテーブルもコピーしています。24ビットの場合にコピーすると例外が発生します。
但しこれだとbmpがメモリリークします。Bitmap bmp = new Bitmap(ms)にusingをつけてDispose()をすると、呼び出し元のBitmap利用で例外が発生します。

```cs:失敗例4：bmpがメモリリークする
                Bitmap bitmap = null;
                using (MemoryStream ms = new MemoryStream(buffer))
                {
                    Bitmap bmp = new Bitmap(ms);

                    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                    try
                    {
                        bitmap = new Bitmap(bmp.Width, bmp.Height, bmpData.Stride, bmp.PixelFormat, bmpData.Scan0);
                    }
                    finally
                    {
                        bmp.UnlockBits(bmpData);
                    }

                    if (_bitmapInfo.BitCount < 24)
                    {
                        bitmap.Palette = bmp.Palette;
                    }
                }

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;
```

このため、bmpDataのScan0をそのまま使用するのではなく、コピーして利用します。

```cs:正しい方法：グレースケールが8ビットのBitmapになる
                Bitmap bitmap = null;
                using (MemoryStream ms = new MemoryStream(buffer))
                using (Bitmap bmp = new Bitmap(ms))
                {
                    BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                    try
                    {
                        bitmap = new Bitmap(bmp.Width, bmp.Height, bmp.PixelFormat);
                        BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
                        try
                        {
                            int bmpBufferSize = bmpData.Stride * bmp.Height;
                            byte[] bmpBuffer = new byte[bmpBufferSize];
                            Marshal.Copy(bmpData.Scan0, bmpBuffer, 0, bmpBufferSize);
                            Marshal.Copy(bmpBuffer, 0, bitmapData.Scan0, bmpBufferSize);
                        }
                        finally
                        {
                            bitmap.UnlockBits(bitmapData);
                        }
                    }
                    finally
                    {
                        bmp.UnlockBits(bmpData);
                    }

                    if (_bitmapInfo.BitCount < 24)
                    {
                        bitmap.Palette = bmp.Palette;
                    }
                }

                bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));

                return bitmap;
```

#5. 結果

これでグレースケールでスキャンしたものが8ビットBitmapになります(Canon DR-G1130で確認)。それをPNGで保存すると8ビットPNGになります。1ビット、24ビットもそれぞれのビット数になります。

#6. TwainDotNet自体を改造しない場合

下記の作業を行うと、TwainDotNet自体は改造せずに済みます。DataSouceManager内のnew BitmapRenderer 1箇所をnew MyBitmapRendererに変更するためだけにこれだけ変更が必要です。

1) 自分のプロジェクトに上記 BitmapFileHeader.cs を追加。
2) TwainDotNetの下記のファイルを自分のプロジェクトにコピーしてリネームする。

　　BitmapRenderer.cs → MyBitmapRenderer.cs
　　DataSource.cs → MyDataSource.cs
　　DataSourceManager.cs → MyDataSourceManager.cs
　　Kernel32Native.cs → MyKernel32Native.cs
　　Twain.cs → MyTwain.cs
　　TwainConstants.cs → MyTwainConstants.cs

3) 上記BitmapRendererの改造をMyBitmapRendererに対して行う。
4) それぞれのクラスの使用箇所に「My」を追加し、念のためnamespaceを変更する。
5) 自分のプロジェクトのTwainの部分をMyTwainに変更する。

継承でもっと短くならないか試したのですが、一部privateメンバやinternalメンバがあるため、ここまでコピーしないと無理でした。また、中途半端に継承すると、どこか失敗したのか、Canonスキャナのダイアログがのっぺらぼうになって操作不能になりました。

ITScan 1.03 20200522(旧版)に対して、TwainDotNetを通常版にしてこの変更を行ったソースを下記の場所に置きました。
[ITScan_src_1.03_TwainDotNet無変更版_20200523.zip](https://1drv.ms/u/s!Ag9NTi4_Aogqll4zDO2cLW-KcJWJ?e=YsTndd)

ITScan 1.04以降はこの変更を行っていて、かつ、MyDataSourceManagerに手を入れています。(スキャンが終了してもスキャナのダイアログが閉じないように変更)

#7. メモリ使用量の削減

上記の方法の場合、メモリリークを回避しつつTransferImageEventArgsでBitmapを返却するために、新たなBitmapを作成して中身をコピーするため、非常にメモリを食います。32ビットTWAINを使用するため呼び出し元が32ビットアプリケーションのため、巨大な画像を読み込むとメモリ不足でnew Bitmapが「使用されたパラメータが有効ではありません。」となります。

これを回避するため、[スキャンアプリケーション ITScan - Qiita](https://qiita.com/mayo00/items/01a22bbe1e9283fce6cb) 1.06以降では、MyTransferImageEventArgsを作成してBitmapオブジェクトではなくHBITMAP(ハンドル)を返却しています。呼び出し元でMyBitmapRendererのコンストラクタとRenderToBitmap()の処理を行い、Bitmapのコピーをしないようにしてメモリ使用量を削減しています。