Search code examples
c#imagedisposememorystream

Why does closing a memory stream used to create an Image object effect a new Image object?


Question:

Why does a MemoryStream object used for ImageA effect a cloned Image object named ImageB? I know Microsoft says that when you create an Image object from a memory stream you have to leave that stream open for the life of the image. But I'm trying to create ImageA from a MemoryStream, clone ImageA to ImageB, then dispose of ImageA and the MemoryStream used to create ImageA. I get errors working with ImageB afterwards.

Source Examples:

All the code below is used to create a new multi-page tiff image from 3 tiff files (single page). Ending up with a single tiff image that is 3 pages. From this you gain...

  • Image newImg
  • MemoryStream ms

Note: This might seem like a lot of code for a small test but I wanted to give a really good example of where I'm getting the objects I'm working with. A lot of this example code was taken from a few different methods/functions and I just changed around a few things to really paint a picture of everything I'm doing from the importing of the file itself.

Image img = Image.FromFile(@"c:\test\page001.tif");
MemoryStream ms = new MemoryStream();
List<Image> imagesToAdd = new List<Image>()
{
    Image.FromFile(@"c:\test\page002.tif"),
    Image.FromFile(@"c:\test\page003.tif")
};

// Create compression encoder parameter
EncoderParameter compressionParam = new EncoderParameter(Encoder.Compression, (long)EncoderValue.CompressionCCITT4);

// Create first page frame parameter
EncoderParameter firstFrameParam = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.MultiFrame);

// Create additional pages frame parameter
EncoderParameter additionalFramesParam = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.FrameDimensionPage);

// Create color depth parameter
EncoderParameter colorDepthParam = new EncoderParameter(Encoder.ColorDepth, (long)1);

// Create flush parameter
EncoderParameter flushParam = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.Flush);

// Create last frame parameter
EncoderParameter lastPageParam = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.LastFrame);

// Create first page encoder parameters
EncoderParameters firstFrameParams = new EncoderParameters(2)
{
    Param = new EncoderParameter[]
    {
            compressionParam,
            firstFrameParam,
    }
};

// Create additional pages encoder parameters
EncoderParameters additionalFrameParams = new EncoderParameters(2)
{
    Param = new EncoderParameter[]
    {
            compressionParam,
            additionalFramesParam,
    }
};

// Create save to file encoder parameters
EncoderParameters saveToFileParams = new EncoderParameters(2)
{
    Param = new EncoderParameter[]
    {
            compressionParam,
            colorDepthParam,
    }
};

// Create flush encoder parameters
EncoderParameters flushParams = new EncoderParameters(1)
{
    Param = new EncoderParameter[]
    {
            flushParam,
            lastPageParam,
    }
};

// Get the tiff image codec
ImageCodecInfo codec = ImageCodecInfo.GetImageEncoders().Where(c => c.MimeType == "image/tiff").First();

// Save the first page to memory stream
img.Save(ms, codec, firstFrameParams);

// Save aditional pages to memory stream
foreach (Image image in imagesToAdd)
    img.SaveAdd(image, additionalFrameParams);

// Finalize the new multi-page image
img.SaveAdd(flushParams);

//Image newImg = pageOne.SaveMultiPageImage(imgCol, out MemoryStream ms);
Image newImg = Image.FromStream(ms);

// Cleanup
firstFrameParams.Dispose();
additionalFrameParams.Dispose();
saveToFileParams.Dispose();
flushParam.Dispose();
compressionParam.Dispose();
firstFrameParam.Dispose();
additionalFramesParam.Dispose();
colorDepthParam.Dispose();
flushParam.Dispose();
lastPageParam.Dispose();

Below code is really used in another place. I have made all this one function for testing to make a good example. The idea with the code below is to work with the image produced with code above, then make a clone of it, delete the original image and the memory stream used to create it, then try to work with the newly created Image object.

Issue: After disposing the memorystream used to create newImg Image object, the new image (newImg2) that was cloned from newImg becomes corrupt.

// Try to work with the newImg Image object (no issues)
int pageCount = newImg.GetFrameCount(FrameDimension.Page);
for (int i = 0; i < pageCount; i++)
{
    newImg.SelectActiveFrame(FrameDimension.Page, i);
    Debug.WriteLine($"BEFORE - Page:{i + 1}  W/H: {newImg.Width}/{newImg.Height}");
}

// Create a new Image (newImg2) based on the original Image (newImg)
// then dispose of the original Image object as it's no longer needed
Image newImg2 = (Image)newImg.Clone();
newImg.Dispose();

// This causes issues when dealing with newImg2 going forward
ms.Dispose();

// Try to work with the newImg2 Image object (issues)
pageCount = newImg2.GetFrameCount(FrameDimension.Page);
for (int i = 0; i < pageCount; i++)
{
    newImg2.SelectActiveFrame(FrameDimension.Page, i);
    Debug.WriteLine($"AFTER - Page:{i + 1}  W/H: {newImg2.Width}/{newImg2.Height}");
}

Solution

  • Using Clone() is like copying a pointer or reference. Both images (the original and the clone) are sharing the same underlying memory/image data in the original MemoryStream.

    Because of this, disposing ms means the memory that holds that image data for both is released and no longer valid. All images that reference this MemoryStream fail when they try to access its content.

    If you intend on closing the memoryStream here, ensure that newImg2 is a completely independent copy.

    using (MemoryStream cloneStream = new MemoryStream())
    {
        newImg.Save(cloneStream, ImageFormat.Tiff);
        tempStream.Position = 0;
        newImg2 = Image.FromStream(cloneStream);
    }
    

    This must be done before calling

    ms.Dispose();
    

    Alternatively, (see comments, and thank you)

    MemoryStream newMs = new MemoryStream(ms.ToArray());
    Image imgClone = Image.FromStream(newMs);
    ms.Dispose();