WPF开发-扫描仪Twain协议Bitmap图片解析

前言

Twain协议扫描图片的时候,图片是以Bitmap的格式存储在内存中,我们需要从内存中把图片给复制出来。

小知识:

1字节 = 8位

首先我们要了解Bitmap的结构

Bitmap结构

BMP文件由文件头、位图信息头、颜色信息和图形数据四部分组成。

image-20240608173606722

文件头

位图文件头BITMAPFILEHEADER,是一个结构,其定义如下:

1
2
3
4
5
6
7
typedef struct tagBITMAPFILEHEADER{
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER;

位图信息头

其中位图的信息头对应的结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[StructLayout(LayoutKind.Sequential, Pack = 2)]
public class Bitmapinfoheader
{
public uint biSize;
public int biWidth;
public int biHeight;
public ushort biPlanes;
public ushort biBitCount;
public uint biCompression;
public uint biSizeImage;
public int biXPelsPerMeter;
public int biYPelsPerMeter;
public uint biClrUsed;
public uint biClrImportant;
}

字段说明

  • biSize:指定这个结构的长度,为40,单位字节
  • biWidth:指定图象的宽度,单位是像素
  • biHeight:指定图象的高度,单位是像素
  • biPlanes:必须是1,不用考虑
  • biBitCount:指定表示颜色时要用到的位数,常用的值为1(黑白二色图),4(16色图),8(256色图),24(真彩色图),新的.bmp格式支持32位图
  • biCompression:指定位图是否压缩,有效的值为BI_RGB,BI_RLE8,BI_RLE4,BI_BITFIELDS(都是一些Windows定义好的常量)。要说明的是,Windows位图可以采用RLE4,和RLE8的压缩格式,但用的不多。我们今后所讨论的只有第一种不压缩的情况,即BI_RGB。
  • biSizeImage:指定实际的位图数据占用的字节数,如果biCompression为BI_RGB,则该项可能为零
  • biXPelsPerMeter:指定目标设备的水平分辨率,单位是每米的象素个数,关于分辨率的概念,我们将在打印部分详细介绍。
  • biYPelsPerMeter:指定目标设备的垂直分辨率,单位同上。
  • biClrUsed:指定本图像实际用到的颜色数,如果该值为0,则用到的颜色数为2的biBitCount次方
  • biClrImportant:指定本图象中重要的颜色数,如果该值为零,则认为所有的颜色都是重要的。

颜色信息

所占字节 = 颜色数 * 4

调色板实际上是一个数组,共有biClrUsed个元素,每个元素占4字节,如果该值为零,则有2的biBitCount次方个元素。

真彩色图,是不需要调色板的,颜色数为0。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int colorNum = 0;
if (bi.biClrUsed != 0)
{
colorNum = (int)bi.biClrUsed;
}
else
{
switch (bi.biBitCount)
{
case 1:
colorNum = 2;
break;
case 4:
colorNum = 16;
break;
case 8:
colorNum = 256;
break;
case 24:
{
colorNum = 0; //对于真彩色图,没用到调色板
break;
}
}
}
int paletteSize = colorNum * 4;

图形数据

字节数为 biSizeImage 的值。

Windows规定一个扫描行所占的字节数必须是4的倍数(即以long为单位),不足的以0填充,

所以图片的尺寸计算公式为

1
biSizeImage = ((((bi.biWidth * bi.biBitCount) + 31) & ~31) / 8) * bi.biHeight;

其中

1
((bi.biWidth * bi.biBitCount) + 31) & ~31

这行代码的含义是将一个图像的每行像素数据的字节数补齐至32位对齐。也就是4字节对齐。

在这段代码中,bi.biWidth 表示图像的宽度,bi.biBitCount 表示每个像素所占的位数。

首先,将每行像素数据的字节数计算为 (bi.biWidth * bi.biBitCount)。然后,加上31,并将结果与31进行与运算,相当于向上取整至32的倍数,以确保每行像素数据结束时是32位对齐的。

TWain扫描的图片

TWain协议保存在内存的Bitmap是不包含文件头的。

只包含位图信息头颜色信息图形数据

内存操作

获取图像句柄的内存指针

1
2
3
4
5
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalLock(IntPtr handle);

[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalUnlock(IntPtr handle);

上面的两个方法分别是使用 P/Invoke 调用 Windows API 中的 GlobalLock 和 GlobalUnlock 函数。

传入的参数都是句柄。

GlobalLock 方法:

  • GlobalLock 函数的作用是将内存对象的句柄转换为指向相应内存块的指针,并增加指定的内存对象的锁定计数。
  • 调用 GlobalLock 函数,将传入的句柄(handle)转换为指向全局内存块的指针,并返回该指针的 IntPtr 类型对象。
  • 这样可以访问和操作全局内存中的数据。

GlobalUnlock 方法:

  • GlobalUnlock 函数的作用是减小指定的内存对象的锁定计数,并将它解锁。
  • 调用 GlobalUnlock 函数,解锁之前通过 GlobalLock 锁定的全局内存块,以释放内存资源并允许其他进程访问该内存块。

这两个函数配合使用,可以在操作全局内存块时进行锁定和解锁操作,确保内存访问的正确性和资源释放的准确性。

如下代码所示

1
2
3
IntPtr bmpPtr = TwainWin32.GlobalLock(dibHandle);
TwainWin32.Bitmapinfoheader bmi = new TwainWin32.Bitmapinfoheader();
Marshal.PtrToStructure(bmpPtr, bmi);

第一行是把内存对象的句柄转换为内存块指针。

解析位图信息头

Marshal.PtrToStructure(bmpPtr, bmi)方法将内存中的数据按照指定的结构体类型进行解析,并将其转换为.NET中的结构体对象。

这里之所以不用传内存的长度,是因为他会自动根据结构体中属性的类型所占字节自动计算。

所以使用Marshal.PtrToStructure获取对象的时候结构体是不能删除属性的也不能修改字段名,会造成解析错误。

图形数据指针

1
IntPtr pixptr = (IntPtr)((int)bmpPtr + bi.biSize + paletteSize);

整个图片的内存指针+位图信息头偏移+颜色信息偏移就是图形数据所在的开始的指针了。

复制图形数据

这种方式复制的图形才是正常的

1
2
3
4
5
6
7
8
9
10
11
12
13
int width = bi.biWidth;
int height = bi.biHeight;
int stride = (int)(bi.biSizeImage / height);
byte[] imageBytes = new byte[stride * height];
for (int i = 0, j = 0, k = (height - 1) * stride; i < height; i++, j += stride, k -= stride)
{
Marshal.Copy(
((IntPtr)((int)pixptr + j)),
imageBytes,
k,
stride
);
}

下面这种方式获取的是翻转的。

1
2
3
4
5
6
7
byte[] imageBytes = new byte[bi.biSizeImage];
Marshal.Copy(
pixptr,
imageBytes,
0,
(int)bi.biSizeImage
);

创建BitmapSource

1
2
3
4
5
6
7
8
9
10
11
12
public static BitmapSource Create(
int pixelWidth,
int pixelHeight,
double dpiX,
double dpiY,
PixelFormat pixelFormat,
BitmapPalette palette,
Array pixels,
int stride)
{
return (BitmapSource) new CachedBitmap(pixelWidth, pixelHeight, dpiX, dpiY, pixelFormat, palette, pixels, stride);
}

参数说明:

  • pixelWidth: 位图的宽度,以像素为单位。

  • pixelHeight: 位图的高度,以像素为单位。

  • dpiX: 位图的水平分辨率,即每英寸水平包含的像素数。

  • dpiY: 位图的垂直分辨率,即每英寸垂直包含的像素数。

  • pixelFormat: 位图的像素格式,指定像素的布局和颜色信息的存储方式。

  • palette: 调色板,如果不使用调色板,则传入 null。

  • pixels: 包含位图像素数据的字节数组。

  • stride: 位图的扫描行宽度,即每行像素数据所占的字节数。

小知识

句柄和指针

内存对象的句柄(handle)和内存对象的指针(pointer)是用于访问内存中对象的两种不同机制。

指针(Pointer)

  • 指针是一个直接指向内存地址的值。它们直接包含要访问的内存地址,因此可以直接用来访问对象或数据。
  • 指针提供了直接的、实时的内存访问。

句柄(Handle)

  • 句柄是一个间接引用的值,它本身不直接指向内存对象,而是用作访问内存对象的标识符或引用。
  • 句柄通常是一个在内存中固定位置的值,可以用来查找实际的内存地址或对象。

关系:

  • 间接性:句柄用于间接访问内存对象,而指针直接指向内存对象。
  • 安全性:句柄可以提供额外的安全性,因为它们可以隐藏实际的内存地址或对象,并且可以进行权限检查。
  • 管理:句柄有助于管理内存对象的生命周期,可以用来实现资源管理和自动化清理。

在实际应用中,句柄和指针通常根据需要和设计选择使用。

例如,Windows操作系统中使用句柄来管理GUI对象,而C语言中使用指针直接访问内存。