Why does Image.SelectActiveFrame fail ("A generic error occured in GDI+") on last frame of a multi-page TIFF unless I use Image.FromFile?

Viewed 1316

Related SO Answer (why is this??)

I'm calling a webservice (that I do not have access to source for) that is returning to me a byte array that is supposed to represent a multi-page TIFF image. (method names changed to preserve employment...)

var tiffBytes = _webServiceClient.GetTiff("someIdentifier");

If I write tiffBytes to a file using standard FileStream, then open the resulting file in something that knows what a TIFF is, there's no problem and I can flip through the pages properly. When I create an Image object from this file directly (Image.FromFile(...)), there is also no problems with the rest of my code. This leads me to believe that the bytes stored in tiffBytes are actually valid.

If I write tiffBytes to a MemoryStream, things get weird:

//Note that this code works just fine if I create "img" with Image.FromFile()
var frameCount = img.GetFrameCount(FrameDimension.Page); // frameCount = 2
img.SelectActiveFrame(FrameDimension.Page, 0); // First frame selected with no issue
img.SelectActiveFrame(FrameDimension.Page, 1); // Exception: "A generic error occured in GDI+"

I've considered that I was creating my MemoryStream incorrectly, but I really don't think I am. I've tried a couple of different ways with the same results; the resulting Image object will display and operate fine until I attempt to move to the last page:

Method 1:

using(var ms = new MemoryStream(tiffBytes))
{
    img = Image.FromStream(ms);
}

Method 2:

using(var ms = new MemoryStream(tiffBytes.Length))
{
    ms.Write(tiffBytes, 0, tiffBytes.Length);
    img = Image.FromStream(ms);
}

So... my question is why does the difference between Image.FromFile() and Image.FromStream() make TIFF's last page/Frame cause an exception when it is loaded?

Side question... Is there some kind of win32 api I can access to see exactly what that exception is?

1 Answers

The comment from Hans Passant was useful but I've found not the total solution.

Image manipulation is a pain especially when you are opening multiple files and creating a merged output.

I have fought for a few days with a feature that takes an existing multi page tif and appends additional pages. These additional pages could be a single page tif, a multi page tif or an image file.

The output replaces the original multipage tif and then deletes the 'new' file that has been appended (you may not want to delete the source files. I did as this was part of a document management system).

The issues occur when opening/closing files and when in the code you should do each to avoid file locking.

The following solution worked well for me. This is written in VB.net 4.6 (comments shown as // as VB comments ' don't format correctly) using old fashioned win form, although this code was in a business layer object.

Note the Opening of each file and the use of Using blocks. If you try to do anything with the file outside of the Using block you will run into trouble. Even though me code loads each page image into a List(of Image) if I place the End Using BEFORE the block creating the final merged output the code can't access the image pages.

 Public Shared Function AppendPage(OriginalDocPath As String, NewPageFilePath As String) As Boolean

    //Define return value, default as fail
    Dim bRet As Boolean = False

    Try
        //get the codec for tiff file
        Dim info As ImageCodecInfo = Nothing
        Dim ice As ImageCodecInfo

        //find TIF in the encoder list
        For Each ice In ImageCodecInfo.GetImageEncoders()

            If ice.MimeType = "image/tiff" Then
                info = ice
            End If

        Next ice //use the 'save' encoder

        //List to store all output pages
        Dim imageList As New List(Of Image)
        Dim frameCount As Integer = 0

        //Set up the 'Save' encoder for multi page tif
        Dim enc As Encoder = Encoder.SaveFlag
        Dim ep As New EncoderParameters(1)
        ep.Param(0) = New EncoderParameter(enc, CLng(EncoderValue.MultiFrame))

        //control for original tif that I want to append to
        Dim oldDoc As Image

        Using fs As Stream = File.OpenRead(OriginalDocPath)

            //read the image file from a Stream
            oldDoc = Bitmap.FromStream(fs)

            //count the pages
            frameCount = oldDoc.GetFrameCount(FrameDimension.Page)
            Dim objGuid As Guid = (oldDoc.FrameDimensionsList(0))
            Dim objDimension As FrameDimension = New FrameDimension(objGuid)

            //load each page into the Image List
            For i As Integer = 0 To frameCount - 1 Step 1

                oldDoc.SelectActiveFrame(objDimension, i)
                Dim nextFrame As Bitmap = New Bitmap(oldDoc)
                imageList.Add(nextFrame)

            Next

            //the End Using will close the first image file
        End Using

        //variables for the appending image file
        Dim newDoc As Image
        Dim bmpFullDoc As Bitmap

        //Open file to append
        Using nStr As Stream = File.OpenRead(NewPageFilePath)

            //read using a Stream
            newDoc = Bitmap.FromStream(nStr)

            //Count the pages in the image (works for tif, jpg, png)
            frameCount = newDoc.GetFrameCount(FrameDimension.Page)
            Dim objGuid As Guid = (newDoc.FrameDimensionsList(0))
            Dim objDimension As FrameDimension = New FrameDimension(objGuid)

            //add each page to the Image List
            For i As Integer = 0 To frameCount - 1 Step 1

                newDoc.SelectActiveFrame(objDimension, i)
                Dim nextFrame As Bitmap = New Bitmap(newDoc)
                imageList.Add(nextFrame)

            Next

            //make a backup of the original file
            File.Copy(OriginalDocPath, OriginalDocPath & "-old", True)
            //delete the original file
            File.Delete(OriginalDocPath)

            Dim firstFrame As Boolean = True

            //create new file with all pages
            For Each im As Image In imageList

                //for the first page we need to actually create the file
                If firstFrame Then
                    //add the first image to the output
                    bmpFullDoc = im

                    //save the first frame
                    bmpFullDoc.Save(OriginalDocPath, info, ep)
                Else
                    //for subsequent pages we need to change the Encoder 'Page'
                    ep.Param(0) = New EncoderParameter(enc, CLng(EncoderValue.FrameDimensionPage))
                    //append next page
                    bmpFullDoc.SaveAdd(im, ep)

                End If
                //tell the loop we've saved the first page already
                firstFrame = False

            Next
        End Using

        //destroy the temp bitmap object
        bmpFullDoc.Dispose()
        //destroy the encoder parameters object
        ep.Dispose()

        //finally, delete the temp and source files
        File.Delete(OriginalDocPath & "-old")
        File.Delete(NewPageFilePath)

        //Set Return value to Success
        bRet = True

    Catch ex As Exception
        //Log error or throw exception
    End Try

    //return success or fail
    Return bRet

End Function
Related