如何使用.NET在内存中动态创建一个JPG图像?

21

我有一个用C#编写的.NET(3.5 SP1)库(DLL)。我需要通过一个类方法扩展此库,该方法将具有以下签名:

public byte[] CreateGridImage(int maxXCells, int maxYCells,
    int cellXPosition, int cellYPosition)
{
    ...
}

这个方法的目的是:

  • 输入参数maxXCellsmaxYCells定义了一个网格,在X和Y方向上的单元格的大小。 maxXCellsmaxYCells分别表示每个方向上单元格的数量。单元格为正方形。(因此它有点像不对称的棋盘。)
  • 输入参数cellXPositioncellYPosition标识了此网格中的一个特殊单元格,该单元格必须填充十字。
  • 真正需要的只是白色背景上黑色网格线和其中一个带有 X 的单元格,无需花哨的图形。
  • 生成的图形必须为jpg格式。
  • 创建此图形必须在内存中进行,并且不应存储在磁盘文件中,也不应在屏幕上绘制。
  • 该方法将以byte[]的形式返回生成的图像。

我非常不熟悉.NET中的图形功能,所以我的问题是:

  • 在不使用第三方库(我想避免使用)的情况下,是否可以使用.NET 3.5 SP1实现这一目标?
  • 我需要遵循哪些基本步骤,以及我需要了解哪些重要的.NET命名空间、类和方法才能实现这个目标(尤其是绘制线条和其他简单的图形元素“在内存中”,并将结果转换为jpg格式的字节数组)?

谢谢您提前的建议!


个体单元格的宽度和高度应该是多少?我们知道它们应该是正方形的,但我们仍然需要确定一个尺寸。 - NakedBrunch
@Alison:单元格的宽度和高度以像素为单位将作为类成员或固定常量值提供给该方法访问。 - Slauma
4个回答

36
下面是一个完整的代码示例,将使用GDI绘制网格并放置一个十字架(红色背景),就像下面的示例图像一样。它与其他答案一样使用GDI,但真正的工作是通过循环遍历单元格并绘制网格线来完成的。
以下是代码:
byte[] bytes = CreateGridImage(10,10, 9, 9, 30);

将创建一个10x10的网格,并在9x9位置放置一个十字形:

在此处输入图像描述

CreateGridImage()函数新增了一个boxSize参数,该参数设置每个格子的大小。

public static byte[] CreateGridImage(
            int maxXCells,
            int maxYCells,
            int cellXPosition,
            int cellYPosition,
            int boxSize)
{
    using (var bmp = new System.Drawing.Bitmap(maxXCells * boxSize+1, maxYCells * boxSize+1))
    {
        using (Graphics g = Graphics.FromImage(bmp))
        {
            g.Clear(Color.Yellow);
            Pen pen = new Pen(Color.Black);
            pen.Width = 1;

            //Draw red rectangle to go behind cross
            Rectangle rect = new Rectangle(boxSize * (cellXPosition - 1), boxSize * (cellYPosition - 1), boxSize, boxSize);
            g.FillRectangle(new SolidBrush(Color.Red), rect);

            //Draw cross
            g.DrawLine(pen, boxSize * (cellXPosition - 1), boxSize * (cellYPosition - 1), boxSize * cellXPosition, boxSize * cellYPosition);
            g.DrawLine(pen, boxSize * (cellXPosition - 1), boxSize * cellYPosition, boxSize * cellXPosition, boxSize * (cellYPosition - 1));

            //Draw horizontal lines
            for (int i = 0; i <= maxXCells;i++ )
            {
                g.DrawLine(pen, (i * boxSize), 0, i * boxSize, boxSize * maxYCells);
            }

            //Draw vertical lines            
            for (int i = 0; i <= maxYCells; i++)
            {
                g.DrawLine(pen, 0, (i * boxSize), boxSize * maxXCells, i * boxSize);
            }                    
        }

        var memStream = new MemoryStream();
        bmp.Save(memStream, ImageFormat.Jpeg);
        return memStream.ToArray();
    }
}

3
太棒了!我真的没有想到会有这么详细的工作。非常感谢你! - Slauma
是的,有很多种方法可以解决这个问题,但上面的代码肯定能帮助你入门。 - NakedBrunch
非常感谢您提供的详细示例 - 对我在类似情况下很有用。 - Jonathan
谢谢。非常棒的例子! - davvs
error CS1069: The type name 'Bitmap' could not be found in the namespace 'System.Drawing'. - john k

7
  1. 创建一个System.Drawing.Bitmap对象。

  2. 创建一个Graphics对象来进行绘制。

  3. 将Bitmap对象保存到MemoryStream中作为JPEG对象。

别忘了在临时位图上调用Dispose!

以下是示例代码,您可以更改像素格式和各种选项,请查看MSDN文档。

    public static byte[] CreateGridImage(
        int maxXCells, 
        int maxYCells,
        int cellXPosition, 
        int cellYPosition)
    {
        // Specify pixel format if you like..
        using(var bmp = new System.Drawing.Bitmap(maxXCells, maxYCells)) 
        {
            using (Graphics g = Graphics.FromImage(bmp))
            {
                // Do your drawing here
            }

            var memStream = new MemoryStream();
            bmp.Save(memStream, ImageFormat.Jpeg);
            return memStream.ToArray();
        }
    }

4

首先,关于绘图,你可以选择:

  • 使用 Graphics 类来使用 GDI 提供的功能
  • 锁定位图并手动绘制

至于保存,你可以使用 MemoryStream 类来存储字节,然后从中获取字节数组。

示例代码可能如下所示(假设你想使用 Graphics 对象在位图上绘制):

public byte[] CreateGridImage(int maxXCells, int maxYCells,
                    int cellXPosition, int cellYPosition)
{
    int imageWidth = 1;
    int imageHeight = 2;
    Bitmap bmp = new Bitmap(imageWidth, imageHeight);

    using (Graphics g = Graphics.FromImage(bmp))
    {
        //draw code in here
    }

    MemoryStream imageStream = new MemoryStream();

    bmp.Save(imageStream, System.Drawing.Imaging.ImageFormat.Jpeg);
    bmp.Dispose();

    return imageStream.ToArray();
}

谢谢!在你的例子中已经把所有重要的组件都放在了一起。为了绘图,我需要像g.DrawLine和Pen和Brush对象等函数,是吗?我不是很理解你提到的这两个选项之间的区别(“使用Graphics…”与“锁定位图…”)。这具体意味着什么,哪个选项更好? - Slauma
是的,DrawLines和Pen/Brush对象非常好用,另一个选择是不使用Graphics,而是自己操作位图的内存。如果您对Graphics对象提供的内容以及其工作速度感到满意,那么您绝对不需要尝试锁定位图。 - Marcin Deptuła
X和Y方向的单元格数量可能始终<= 60(因此在最坏的情况下我必须画120条线),并且此函数每小时调用一次左右。因此,图形操作不会很大,我想。这不可能是性能问题,对吧? - Slauma
在这种情况下 - 不行。如果你想瞄准巨大的位图或者实时/近实时渲染(比如每秒10帧),那么你可以开始考虑其他选项 :)。 - Marcin Deptuła

2

Slauma,

这里介绍另一种方法,使用WindowsForm的DataGridView控件来绘制网格。

    public byte[] GetData()
    {
        Form form = new Form();
        //Create a new instance of DataGridView(WindowsForm) control.
        DataGridView dataGridView1 = new DataGridView();
        form.Controls.Add(dataGridView1);

        //Customize output.
        dataGridView1.RowHeadersVisible = false;
        dataGridView1.ColumnHeadersVisible = false;
        dataGridView1.ScrollBars = ScrollBars.None;
        dataGridView1.AutoSize = true;

        //Set datasource.
        dataGridView1.DataSource = GetDataTable();

        //Export as image.
        Bitmap bitmap = new Bitmap(dataGridView1.Width, dataGridView1.Height);
        dataGridView1.DrawToBitmap(bitmap, new Rectangle(Point.Empty, dataGridView1.Size));
        //bitmap.Save("sample.jpg", System.Drawing.Imaging.ImageFormat.Jpeg);

        MemoryStream ms = new MemoryStream();
        bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);

        bitmap.Dispose();
        form.Dispose();

        return ms.ToArray();
    }

    /// <summary>
    /// Helper method.
    /// </summary>
    DataTable GetDataTable()
    {
        DataTable dt = new DataTable();

        for (int i = 0; i < 2; i++)
            dt.Columns.Add(string.Format("Column{0}", i));

        for (int i = 0; i < dt.Columns.Count; i++)
        {
            for (int j = 0; j < 10; j++)
            {
                dt.Rows.Add(new string[] { "X1", "Y1" });
            }
        }

        return dt;
    }

在客户端的app.config文件中(替换这一行): 祝编码愉快!

嗯,有趣的解决方案!但我不在Windows Forms应用程序中,目前我没有引用任何Windows Forms程序集。将包含此新函数的库是WCF服务的一部分。您的解决方案适用于这种情况吗? - Slauma
@Slauma,这个解决方案在WCF下也可以工作,我刚测试过。但是你需要在你的项目中添加对“System.Windows.Forms.dll”和“System.Drawing.dll”的引用。干杯。 - Karthik Mahalingam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接