在 .Net 中 System.Drawing.Image.Save 是确定性的吗?

In .Net is System.Drawing.Image.Save deterministic?

我正在尝试通过字节内容比较两个图像。但是,它们不匹配。

两张图片都是从同一个源图片生成的,使用相同的方法和相同的参数。我猜测图像生成中的某些内容或我转换为字节数组的方式不是确定性的。有谁知道非确定性行为发生在哪里以及我是否可以轻松地为我的单元测试强制确定性行为?

我测试中的这个方法 class 将图像转换为字节数组 - image.Save 是确定性的吗? memStream.ToArray() 是确定性的吗?

private static byte[] ImageToByteArray(Image image)
{
    byte[] actualBytes;
    using (MemoryStream memStream = new MemoryStream())
    {
        image.Save(memStream, ImageFormat.Bmp);
        actualBytes = memStream.ToArray();
    }
    return actualBytes;
}

这是单元测试,它失败了 - TestImageLandscapeDesertResized_300_300 是使用 ImageHelper.ResizeImage(testImageLandscape, 300, 300)TestImageLandscapeDesert 生成的,然后在加载到项目的资源文件之前保存到文件中。如果我的代码中的所有调用都是基于我的输入参数的确定性,则该测试应该通过。

public void ResizeImage_Landscape_SmallerLandscape()
{
    Image testImageLandscape = Resources.TestImageLandscapeDesert;
    Image expectedImage = Resources.TestImageLandscapeDesertResized_300_300;
    byte[] expectedBytes = ImageToByteArray(expectedImage);
    byte[] actualBytes;
    using (Image resizedImage = ImageHelper.ResizeImage(testImageLandscape, 300, 300))
    {
        actualBytes = ImageToByteArray(resizedImage);
    }
    Assert.IsTrue(expectedBytes.SequenceEqual(actualBytes));
}

正在测试的方法 - 此方法将缩小输入图像,使其高度和宽度小于 maxHeightmaxWidth,并保留现有的纵横比。某些图形调用可能是不确定的,我无法从 Microsoft 有限的文档中判断出来。

public static Image ResizeImage(Image image, int maxWidth, int maxHeight)
{
    decimal width = image.Width;
    decimal height = image.Height;
    decimal newWidth;
    decimal newHeight;

    //Calculate new width and height
    if (width > maxWidth || height > maxHeight)
    {
        // need to preserve the original aspect ratio
        decimal originalAspectRatio = width / height;

        decimal widthReductionFactor = maxWidth / width;
        decimal heightReductionFactor = maxHeight / height;

        if (widthReductionFactor < heightReductionFactor)
        {
            newWidth = maxWidth;
            newHeight = newWidth / originalAspectRatio;
        }
        else
        {
            newHeight = maxHeight;
            newWidth = newHeight * originalAspectRatio;
        }
    }

    else
        //Return a copy of the image if smaller than allowed width and height
        return new Bitmap(image);

    //Resize image
    Bitmap bitmap = new Bitmap((int)newWidth, (int)newHeight, PixelFormat.Format48bppRgb);
    Graphics graphic = Graphics.FromImage(bitmap);
    graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
    graphic.DrawImage(image, 0, 0, (int)newWidth, (int)newHeight);
    graphic.Dispose();

    return bitmap;
}

这最终奏效了。我不知道这对单元测试来说是否是个好主意,但由于 GDI+ 逻辑是不确定的(或者我的逻辑与之交互),这似乎是最好的方法。

我使用 MS Fakes Shimming 功能来填充相关调用并验证预期值是否已传递给被调用的方法。然后我调用本机方法以获取被测方法的其余部分所需的功能。最后我验证了返回图像的一些属性。

不过,我更愿意将预期输出与实际输出进行直接比较...

[TestMethod]
[TestCategory("ImageHelper")]
[TestCategory("ResizeImage")]
public void ResizeImage_LandscapeTooLarge_SmallerLandscape()
{
    Image testImageLandscape = Resources.TestImageLandscapeDesert;

    const int HEIGHT = 300;
    const int WIDTH = 300;
    const int EXPECTED_WIDTH = WIDTH;
    const int EXPECTED_HEIGHT = (int)(EXPECTED_WIDTH / (1024m / 768m));
    const PixelFormat EXPECTED_FORMAT = PixelFormat.Format48bppRgb;
    bool calledBitMapConstructor = false;
    bool calledGraphicsFromImage = false;
    bool calledGraphicsDrawImage = false;

    using (ShimsContext.Create())
    {
        ShimBitmap.ConstructorInt32Int32PixelFormat = (instance, w, h, f) => {
            calledBitMapConstructor = true;
            Assert.AreEqual(EXPECTED_WIDTH, w);
            Assert.AreEqual(EXPECTED_HEIGHT, h);
            Assert.AreEqual(EXPECTED_FORMAT, f);
            ShimsContext.ExecuteWithoutShims(() => {
                ConstructorInfo constructor = typeof(Bitmap).GetConstructor(new[] { typeof(int), typeof(int), typeof(PixelFormat) });
                Assert.IsNotNull(constructor);
                constructor.Invoke(instance, new object[] { w, h, f });
            });
        };
        ShimGraphics.FromImageImage = i => {
            calledGraphicsFromImage = true;
            Assert.IsNotNull(i);
            return ShimsContext.ExecuteWithoutShims(() => Graphics.FromImage(i));
        };
        ShimGraphics.AllInstances.DrawImageImageInt32Int32Int32Int32 = (instance, i, x, y, w, h) => {
            calledGraphicsDrawImage = true;
            Assert.IsNotNull(i);
            Assert.AreEqual(0, x);
            Assert.AreEqual(0, y);
            Assert.AreEqual(EXPECTED_WIDTH, w);
            Assert.AreEqual(EXPECTED_HEIGHT, h);
            ShimsContext.ExecuteWithoutShims(() => instance.DrawImage(i, x, y, w, h));
        };
        using (Image resizedImage = ImageHelper.ResizeImage(testImageLandscape, HEIGHT, WIDTH))
        {
            Assert.IsNotNull(resizedImage);
            Assert.AreEqual(EXPECTED_WIDTH, resizedImage.Size.Width);
            Assert.AreEqual(EXPECTED_HEIGHT, resizedImage.Size.Height);
            Assert.AreEqual(EXPECTED_FORMAT, resizedImage.PixelFormat);
        }
    }

    Assert.IsTrue(calledBitMapConstructor);
    Assert.IsTrue(calledGraphicsFromImage);
    Assert.IsTrue(calledGraphicsDrawImage);
}

这个 table 有点晚了,但添加这个以防它能帮助任何人。在我的单元测试中,这可靠地比较了我使用 GDI+ 动态生成的图像。

private static bool CompareImages(string source, string expected)
{
    var image1 = new Bitmap($".\{source}");
    var image2 = new Bitmap($".\Expected\{expected}");

    var converter = new ImageConverter();
    var image1Bytes = (byte[])converter.ConvertTo(image1, typeof(byte[]));
    var image2Bytes = (byte[])converter.ConvertTo(image2, typeof(byte[]));

    // ReSharper disable AssignNullToNotNullAttribute
    var same = image1Bytes.SequenceEqual(image2Bytes);
    // ReSharper enable AssignNullToNotNullAttribute

    return same;
}