65.9K
CodeProject 正在变化。 阅读更多。
Home

一个支持格式转换、从 URL 获取位图、叠加等功能的位图处理类。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (55投票s)

2003年9月1日

13分钟阅读

viewsIcon

354184

downloadIcon

2171

本文概述并讨论了作者的 BitmapManipulator C# 类,包括每个功能的示例,以及许多 .NET/GDI+ 的注意事项。

Sample Image - bitmapmanip.jpg

摘要

随着 GDI+ API 的发布,微软极大地增强了其图形 API 的功能和灵活性,同时也增加了本已模糊的 GDI API 的复杂性和表面积。幸运的是,.NET Framework 提供了 System.Drawing 命名空间层次结构,以(某种程度上)可控的接口封装了大部分 GDI+ API。不幸的是,许多复杂性仍然存在,因此许多简单的图像处理任务,例如调整位图大小、裁剪区域或在一种格式与其他格式之间转换,要么很复杂,要么不显而易见,有时两者兼而有之。

在本文中,我将介绍我用 C# 编写的 BitmapManipulator 类,它实现了许多常见的图像处理函数,以及更高级的功能,如将叠加图像 alpha 混合到一个位图上,同时隐藏了 GDI+ 的复杂细节。我最初开发这个类是为了用于一个基于 Web 的相册应用程序,因此强调简单易用性和性能。尽管有其起源,但该类及其代码应该适用于各种应用程序,如源代码存档中包含的 Windows Forms 测试程序所示。

引言

本文介绍了 BitmapManipulator 类,回顾了其功能,并带领读者了解源代码的关键元素,指出有趣的技巧或注意事项。对于只想在应用程序中使用该类的读者,可以跳过代码讲解部分,但鼓励所有读者阅读全文。

随附的源代码存档包含 Visual Studio.NET 2002 项目文件,但源代码应该可以与 VS.NET 2K2 或 VS.NET 2K3 配合使用。

背景

BitmapManipulator 是基于 .NET Framework 的 GDI+ 包装类构建的,这些类位于 System.Drawing 命名空间中。虽然对 System.Drawing 进行全面概述远超本文范围,但做一个粗略的回顾可能会很有用。

所有图像都由 System.Drawing.Image 类或其派生类表示。 Image 提供了适用于所有图像类型的基本属性和方法。它的子类 System.Drawing.BitmapSystem.Drawing.Imaging.Metafile 分别为位图图像(即光栅图像;由像素描述的图像)和图元文件图像提供了附加功能。在本文的其余部分,我们将重点关注 Bitmap 类,该类不仅用于表示 BMP 文件,还用于表示 JPEG、GIF、PNG、TIFF 以及 GDI+ 支持的所有其他光栅文件格式。

除了 Bitmap 类之外,System.Drawing.GraphicsBitmapManipulator 的代码中也占有重要地位。Graphics 是用于表示“显示设备”的抽象,但在这种情况下,“显示设备”的含义比监视器更广。概念上,在 GDI+ 中,位图的几乎所有操作(如旋转、裁剪等)实际上都不是直接在 Bitmap 对象上执行的,而是在 Graphics 对象上执行的。反过来,该 Graphics 对象可以表示屏幕、打印机或(在我们的例子中)Bitmap 对象中的图像内容。熟悉 Win32 GDI API 的人会认出 Graphics 是 GDI 设备上下文 (DC) 的面向对象形式。

除了 BitmapGraphics 之外,下面还会遇到一些枚举,如 PixelFormat,以及类,如 ImageFormat。幸运的是,这些实例大多数都相当容易理解,因此上面的概述足以开始回顾 BitmapManipulator 的实现。

BitmapManipulator 结构

BitmapManipulator 是一个 static 类,在一个 C# 源文件中实现。从设计角度来看,它遵循我同事 Richard Cooper 所称的“函数桶”模式;也就是说,它只是公开完全自包含的 static 方法,这些方法最多只与其他方法有浅层关联。虽然这种模式在某些情况下可能不理想,但在本例中,它造就了一个连贯、简洁、易于使用的类,可以轻松地集成到任何地方。

一般来说,每个函数都对位图执行某种操作。为此,每个函数至少接受一个 Bitmap 对象,以及执行操作所需的任何参数。此外,每个函数都返回一个 Bitmap,该位图始终是与传入的位图不同的新实例。在我编写 BitmapManipulator 时,由于一些不常用的原因需要此功能,但在其他情况下它也可能是有利的。然而,必须记住,当不再需要输入 Bitmap 时,要调用 Dispose(),以最有效地利用资源。

下面是一个 BitmapManipulator 函数的示例。

public static Bitmap ScaleBitmap(Bitmap inputBmp, double scaleFactor)

在这种情况下,除了输入的 Bitmap 参数之外,还有一个缩放因子用于控制要执行的操作。绝大多数情况下,BitmapManipulator 的大多数方法都采用这种形式。

位图操作

GetBitmapFromUri

public static Bitmap GetBitmapFromUri(String uri);
public static Bitmap GetBitmapFromUri(Uri uri);
 
public static Bitmap GetBitmapFromUri(String uri, int timeoutMs);
public static Bitmap GetBitmapFromUri(Uri uri, int timeoutMs);

虽然提供了此方法的各种重载形式,但它们都执行相同的任务:从 URI 中检索图像文件,将该文件加载到 Bitmap 中,并返回所得对象。

这些方法中唯一值得注意的一点是对 WebRequest/WebResponse 类及其相关异常的包装:在我编写 BitmapManipulator 时,我需要为用户提供有意义的错误消息,以便在图像下载失败时显示,因此在这些方法中,我捕获与常规 HTTP 错误相关的异常,并将它们打包到一个通用的 BitmapManipException 中,并附带一个(相对)用户友好的错误消息。当然,原始异常始终可以通过 InnerException 属性访问,但此功能使得将图像下载功能集成到应用程序中变得容易,而不会向用户暴露任何尖锐的边缘。

ConvertBitmap

public static Bitmap ConvertBitmap(Bitmap inputBmp, String destMimeType);
public static Bitmap ConvertBitmap(Bitmap inputBmp, 
       System.Drawing.Imaging.ImageFormat destFormat);

顾名思义,此方法的所有重载都用于将一种位图格式转换为另一种格式(例如,JPEG 到 GIF 或 TIFF 到 PNG)。执行此操作所需的代码是 GDI+ 中常见的简单但出人意料的复杂操作的示例。

//Create an in-memory stream which will be used to save
//the converted image
System.IO.Stream imgStream = new System.IO.MemoryStream();
//Save the bitmap out to the memory stream, 
//using the format indicated by the caller
inputBmp.Save(imgStream, destFormat);

//At this point, imgStream contains the binary form of the
//bitmap in the target format.  All that remains is to load it
//into a new bitmap object
Bitmap destBitmap = new Bitmap(imgStream);

没错;图像被保存到内存流中,然后从流中加载到新的 Bitmap 对象中。荒谬,但有效。

ConvertBitmapToJpeg

public static Bitmap ConvertBitmapToJpeg(Bitmap inputBmp, int quality);

作为 ConvertBitmap 的一个特殊情况,此函数将位图转换为 JPEG,并可选择指定 JPEG 编码器的质量参数。此参数在 0(严重失真,极佳压缩)到 100(无损,最小压缩)之间。我最初需要此功能是因为,在我的相册应用程序中,我想将用户照片的质量强制设置为 50 或更低,以最大限度地减少磁盘空间使用。将 quality 传递 -1 等同于使用 ConvertBitmap 并将目标格式设置为 JPEG。

不幸的是,为 JPEG 编码器指定质量参数并非易事,也并非显而易见。由于这似乎是一个经常被问到的问题,因此很有必要展示所需的代码。

//Create an in-memory stream which will be used to save
//the converted image
System.IO.Stream imgStream = new System.IO.MemoryStream();

//Get the ImageCodecInfo for the desired target format
ImageCodecInfo destCodec = FindCodecForType
        (MimeTypeFromImageFormat(ImageFormat.Jpeg));
if (destCodec == null) {
    //No codec available for that format
    throw new ArgumentException("The requested format " +
        MimeTypeFromImageFormat(ImageFormat.Jpeg) +
        " does not have an available codec installed",
        "destFormat");
}

//Create an EncoderParameters collection to contain the
//parameters that control the dest format's encoder
EncoderParameters destEncParams = new EncoderParameters(1);

//Use quality parameter
EncoderParameter qualityParam = new 
        EncoderParameter(Encoder.Quality, quality);
destEncParams.Param[0] = qualityParam;

//Save w/ the selected codec and encoder parameters
inputBmp.Save(imgStream, destCodec, destEncParams);

//At this point, imgStream contains the binary form of the
//bitmap in the target format.  All that remains is to load it
//into a new bitmap object
Bitmap destBitmap = new Bitmap(imgStream);

总而言之,您必须找到目标格式(在此例中为 JPEG)的 ImageCodecInfo 对象,知道未公开的 JPEG 编码器接受一个名为 Encoder.Quality 的参数,其值在 0 到 100 之间,创建一个 EncoderParameter 对象来表示此参数,以及一个 EncoderParameters 集合来包含 EncoderParameterObject。最后,将 ImageCodecInfoEncoderParameters 对象传递给 Bitmap 类的 Save 方法,即可完成。

ConvertBitmapToTiff

public static Bitmap ConvertBitmapToTiff(Bitmap inputBmp, 
          TiffCompressionEnum compression);

概念上,此方法与 ConvertBitmapToJpeg 相同,只是此方法将位图转换为 TIFF 格式。虽然 TIFF 不支持 JPEG 编码器提供的质量参数,但它支持基于图像颜色深度的多种压缩算法。因此,ConvertBitmapToTiff 不是使用质量参数,而是接受一个 BitmapManipulator.TiffCompressionEnum 类型的参数,该参数可以具有以下任何值:

  • CCITT3
  • CCITT4
  • LZW
  • RLE
  • Unspecified(未指定)

请注意,CCITT3CCITT4RLE 在处理 24 或 32 位 TIFF 文件时似乎无法正常工作;会从 GDI+ 内部引发异常。考虑到 GDI+ 的这一领域几乎没有文档记录,一些怪癖是意料之中的。可以使用源代码存档中包含的示例应用程序比较容易地探索此问题。

传递 TiffCompressionEnum.Unspecified 等同于调用 ConvertBitmap 并将 TIFF 指定为目标格式。

ConvertBitmapToTiff 中使用的代码与 ConvertBitmapToJpeg 相同,只是魔术参数是压缩而不是质量。

ScaleBitmap

public static Bitmap ScaleBitmap(Bitmap inputBmp, double scaleFactor);
public static Bitmap ScaleBitmap(Bitmap inputBmp, 
           double xScaleFactor, double yScaleFactor);

显然,此函数通过缩放因子缩放位图的尺寸。传递 1.0 会返回输入位图的精确副本,2.0 会生成原始位图两倍大小的位图,0.5 会生成一半大小的位图,依此类推。

ScaleBitmap 的实现值得注意的是其反直觉之处。

//Create a new bitmap object based on the input
Bitmap newBmp = new Bitmap(
    (int)(inputBmp.Size.Width*xScaleFactor),
    (int)(inputBmp.Size.Height*yScaleFactor),
    PixelFormat.Format24bppRgb);
    //Graphics.FromImage doesn't like Indexed pixel format
    
    
//Create a graphics object attached to the new bitmap
Graphics newBmpGraphics = Graphics.FromImage(newBmp);

//Set the interpolation mode to high quality bicubic
//interpolation, to maximize the quality of the scaled image
newBmpGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
newBmpGraphics.ScaleTransform((float)xScaleFactor, (float)yScaleFactor);

//Draw the bitmap in the graphics object, which will apply
//the scale transform
//Note that pixel units must be specified to 
//ensure the framework doesn't attempt
//to compensate for varying horizontal resolutions 
//in images by resizing; in this case,
//that's the opposite of what we want.
Rectangle drawRect = new Rectangle(0, 0, 
       inputBmp.Size.Width, inputBmp.Size.Height);
newBmpGraphics.DrawImage(inputBmp, drawRect, 
       drawRect, GraphicsUnit.Pixel);

//Return the bitmap, as the operations on the graphics object
//are applied to the bitmap
newBmpGraphics.Dispose();

//newBmp will have a RawFormat of MemoryBmp because it was created
//from scratch instead of being based on inputBmp. 
//Since it it inconvenient
//for the returned version of a bitmap to be of a 
//different format, now convert
//the scaled bitmap to the format of the source bitmap
return ConvertBitmap(newBmp, inputBmp.RawFormat);

在这里,创建了一个 Graphics 对象,并将其绑定到一个具有与输入位图相同尺寸的新空白位图上,并根据缩放因子进行了缩放。插值模式已设置为适当的模式,在本例中是速度最慢但质量最高的一种。接下来,在 Graphics 对象上调用 ScaleTransform,并将 x 和 y 缩放因子作为参数传递。请注意,此时,输入图像尚未复制到 Graphics 对象;相反,ScaleTransformGraphics 对象的“管道”添加了一个变换,因此对 Graphics 对象的任何未来操作都将经历一个缩放。这是一个强大的概念,但最初是反直觉的。

最后,将输入图像绘制到 Graphics 对象上,由于缩放变换已安装,因此图像将在渲染到先前创建的空白位图之前进行缩放。

这种变换管道的概念是 GDI+ 支持的许多变换的核心。

ResizeBitmap

public static Bitmap ResizeBitmap(Bitmap inputBmp, 
                       int imgWidth, int imgHeight);

ResizeBitmap 的实现非常简单。

//Simply compute scale factors that result in the desired size, 
//then call ScaleBitmap
return ScaleBitmap(inputBmp,
    (float)imgWidth/(float)inputBmp.Size.Width,
    (float)imgHeight/(float)inputBmp.Size.Height);

由于 GDI+ 不提供 resize 变换的概念,因此必须通过缩放因子来实现调整大小。由于浮点数的精度,经过四舍五入后,生成的图像可能会有一像素的偏差,但对于大多数应用程序来说,这并不关键。

ThumbnailBitmap

到目前为止,我们检查过的所有方法都有些平庸。而此方法则提供了一个非常有用且有些晦涩的功能:给定一个位图和一个边界矩形,返回输入位图的副本,该副本被缩放到可以容纳在该边界矩形内的最大尺寸,而不会改变图像的宽高比。

再次,我为我的相册应用程序创建了此功能,以便我可以显示一个缩略图表,其中每个表单元格的大小都统一。

此方法实现本身并不值得注意;正如人们所料,它依赖于 ScaleBitmap 来完成繁重的工作。

RotateBitmapRight

public static Bitmap RotateBitmapRight90(Bitmap inputBmp);
public static Bitmap RotateBitmapRight180(Bitmap inputBmp);
public static Bitmap RotateBitmapRight270(Bitmap inputBmp);

将位图分别向右旋转 90、180 和 270 度。令人惊讶的是,实现非常简单直接,在处理 GDI+ 时这是一种例外。考虑 RotateBitmapRight90

//Copy bitmap
Bitmap newBmp = (Bitmap)inputBmp.Clone();
newBmp.RotateFlip(RotateFlipType.Rotate90FlipNone);

//The RotateFlip transformation converts bitmaps to memoryBmp,
//which is uncool.  Convert back now
return ConvertBitmap(newBmp, inputBmp.RawFormat);

令人惊讶的是,Bitmap 类提供了一个 RotateFlip 方法,该方法执行旋转。不需要 Graphics 对象或繁琐的其他代码行。幸运的是,这种直观性本身是异常的,这使得一些混淆得以保留。

ReverseBitmap 和 FlipBitmap

public static Bitmap ReverseBitmap(Bitmap inputBmp);
public static Bitmap FlipBitmap(Bitmap inputBmp);

分别反转(镜像)和翻转(上下颠倒)图像。这些也使用了 Bitmap.RotateFlip 方法,几乎仅此而已。

CropBitmap

public static Bitmap CropBitmap(Bitmap inputBmp, Rectangle cropRectangle);

裁剪输入图像,返回一个包含输入位图中由裁剪矩形包围的部分的位图。此方法的实现是另一个简单但出人意料的解决方案:

//Create a new bitmap object based on the input
Bitmap newBmp = new Bitmap(cropRectangle.Width,
    cropRectangle.Height,
    PixelFormat.Format24bppRgb);
    //Graphics.FromImage doesn't like Indexed pixel format

//Create a graphics object and attach it to the bitmap
Graphics newBmpGraphics = Graphics.FromImage(newBmp);

//Draw the portion of the input image in the crop rectangle
//in the graphics object
newBmpGraphics.DrawImage(inputBmp,
    new Rectangle(0, 0, cropRectangle.Width, cropRectangle.Height),
    cropRectangle,
    GraphicsUnit.Pixel);

//Return the bitmap
newBmpGraphics.Dispose();

//newBmp will have a RawFormat of MemoryBmp because it was created
//from scratch instead of being based on inputBmp. 
//Since it it inconvenient
//for the returned version of a bitmap to be 
//of a different format, now convert
//the scaled bitmap to the format of the source bitmap
return ConvertBitmap(newBmp, inputBmp.RawFormat);

请注意,GDI+ 并不提供任何显式的裁剪支持。相反,会创建一个新的位图,其尺寸与裁剪矩形相同。然后,将一个 Graphics 对象绑定到此新 Bitmap,并调用 Graphics.DrawImage 将裁剪矩形内的输入图像数据绘制到 Graphics 对象上,从而绘制到新位图上。简单,但不直观。

OverlayBitmap

public static Bitmap OverlayBitmap(Bitmap destBmp, 
       Bitmap bmpToOverlay, Point overlayPoint);
public static Bitmap OverlayBitmap(Bitmap destBmp, 
       Bitmap bmpToOverlay, ImageCornerEnum corner);
public static Bitmap OverlayBitmap(Bitmap destBmp, 
       Bitmap bmpToOverlay, int overlayAlpha, Point overlayPoint);
public static Bitmap OverlayBitmap(Bitmap destBmp, 
       Bitmap bmpToOverlay, int overlayAlpha, ImageCornerEnum corner);

此方法是使此类有别于无数其他 C# 图像类的主要元素。通过此方法,可以将一个(假定较小)位图叠加到另一个(假定较大)位图上,可以放在角落、中心或任意点,并带有任意 alpha(透明度)。此功能是为了让我的相册应用程序可以在每张照片上放置一个小的、半透明的水印。

使用此方法非常简单:传递放置叠加图像的图像、要叠加的图像、一些指定叠加图像位置的说明,以及一个可选的 alpha 值(0 到 100;0 为透明,100 为不透明)。

此方法的实现绝非直观。虽然将一个图像叠加到另一个图像上相对简单(只需将 Graphics 对象绑定到输入位图,然后调用 Graphics.DrawImage 来复制叠加图像),但包含叠加的透明度则相当复杂。

//Convert alpha to a 0..1 scale
float overlayAlphaFloat = (float)overlayAlpha / 100.0f;

//Copy the destination bitmap
//NOTE: Can't clone here, because if destBmp is indexed instead of just RGB,
//Graphics.FromImage will fail
Bitmap newBmp = new Bitmap(destBmp.Size.Width,
    destBmp.Size.Height);

//Create a graphics object attached to the bitmap
Graphics newBmpGraphics = Graphics.FromImage(newBmp);

//Draw the input bitmap into this new graphics object
newBmpGraphics.DrawImage(destBmp,
    new Rectangle(0, 0,
                destBmp.Size.Width,
                destBmp.Size.Height),
    0, 0, destBmp.Size.Width, destBmp.Size.Height,
    GraphicsUnit.Pixel);

//Create a new bitmap object the same size as the overlay bitmap
Bitmap overlayBmp = new Bitmap(bmpToOverlay.Size.Width, 
                                bmpToOverlay.Size.Height);

//Make overlayBmp transparent
overlayBmp.MakeTransparent(overlayBmp.GetPixel(0,0));

//Create a graphics object attached to the bitmap
Graphics overlayBmpGraphics = Graphics.FromImage(overlayBmp);

首先,创建一个与输入位图尺寸相同的新 Bitmap 对象。将一个 Graphics 对象绑定到 Bitmap 对象,并使用 DrawImage(Unscaled) 将输入 Bitmap 复制到新 Bitmap 对象。同样,为叠加位图创建一个新的 Bitmap 对象,将叠加位图中的第一个像素设置为透明像素(这是可选的;如果叠加位图不应该有透明背景,则删除它),并将一个 Graphics 对象绑定到叠加 Bitmap 的副本。

//Create a color matrix which will be applied to the overlay bitmap
//to modify the alpha of the entire image
float[][] colorMatrixItems = {
    new float[] {1, 0, 0, 0, 0},
    new float[] {0, 1, 0, 0, 0},
    new float[] {0, 0, 1, 0, 0},
    new float[] {0, 0, 0, overlayAlphaFloat, 0},
    new float[] {0, 0, 0, 0, 1}
};
ColorMatrix colorMatrix = new ColorMatrix(colorMatrixItems);

//Create an ImageAttributes class to contain a color matrix attribute
ImageAttributes imageAttrs = new ImageAttributes();
imageAttrs.SetColorMatrix(colorMatrix, 
       ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

//Draw the overlay bitmap into the graphics object, 
//applying the image attributes
//which includes the reduced alpha
Rectangle drawRect = new Rectangle(0, 0, 
       bmpToOverlay.Size.Width, bmpToOverlay.Size.Height);
overlayBmpGraphics.DrawImage(bmpToOverlay,
    drawRect,
    0, 0, bmpToOverlay.Size.Width, bmpToOverlay.Size.Height,
    GraphicsUnit.Pixel,
    imageAttrs);
overlayBmpGraphics.Dispose();

接下来,创建一个 5x5 矩阵,该矩阵由单位矩阵组成,其中单元格 (4,4) 设置为从 0 到 1 缩放的 alpha 值。任何具有线性代数经验的读者都会认出这里正在构建一个线性变换,尽管其目的可能尚不清楚。

此矩阵用于创建一个 ImageAttributes 对象,并将其传递给 ImageAttributes.SetColorMatrix。该矩阵有效地编码了图像中每个像素颜色的变换,其中颜色是一个五元组,第四个元素是 alpha。再次,一些线性代数经验将使读者得出结论,所有这些矩阵工作都是一种冗长的方式来表示“将 alpha 通道按缩放因子(0-1)进行缩放”。

最后,使用熟悉的 Graphics.DrawImage 将叠加 Bitmap 绘制到叠加副本 Bitmap 中,将叠加 Bitmap 通过颜色变换矩阵,因此结果是原始叠加位图,但透明度信息根据 alpha 值设置。

//overlayBmp now contains bmpToOverlay w/ the alpha applied.
//Draw it onto the target graphics object
//Note that pixel units must be specified 
//to ensure the framework doesn't attempt
//to compensate for varying horizontal resolutions 
//in images by resizing; in this case,
//that's the opposite of what we want.
newBmpGraphics.DrawImage(overlayBmp,
    new Rectangle(overlayPoint.X, overlayPoint.Y, 
    bmpToOverlay.Width, bmpToOverlay.Height),
    drawRect,
    GraphicsUnit.Pixel);
newBmpGraphics.Dispose();

//Recall that newBmp was created as a memory bitmap; 
//convert it to the format
//of the input bitmap
return ConvertBitmap(newBmp, destBmp.RawFormat);

现在,给定一个带有透明度的叠加 Bitmap 版本,另一个 Graphics.DrawImage 调用将其叠加到输入位图上。除了清理和形式化之外,这就是半透明叠加的全部内容。

显然,这是 GDI+ 混淆和复杂操作中最痛苦的例子。然而,最终结果是输入位图和叠加位图的专业、引人注目的混合,非常值得付出努力。

杂项

一些其他公共方法用于 MIME 类型转换等,但它们不执行任何实质性的位图操作。

示例应用程序

本文源代码存档中包含一个简单的 C# Windows Forms 应用程序,它演示了 BitmapManipulator 中的所有功能。请注意,此应用程序是为了演示 BitmapManipulator 类的使用并为读者提供即时满足感而编写的,而不是为了展示作者在 Windows Forms 开发方面的精湛技艺。错误处理和输入验证已被省略,UI 清理留给读者作为练习。

结论

本文介绍了 BitmapManipulator 类,并深入探讨了它在 GDI+ 中的内部工作原理。希望读者在此过程中对基本的 GDI+ 编程概念有所了解,或者至少对未来与 GDI+ 的任何接触都产生了健康的规避。鼓励有雄心的读者扩展 BitmapManipulator 以执行其他有用的功能。特别是,即将发布的文章将探讨在不同颜色深度之间进行抖动的问(net.wrapper 封装 GDI+ 的功能缺失)。

历史

  • 2003-9-1 首次发布
© . All rights reserved.