0

First, I save the frames of a tiff into a list of bitmaps:

public Bitmap SaveTiffAsTallPng(string filePathAndName)
{
    byte[] tiffFile = System.IO.File.ReadAllBytes(filePathAndName);
    string fileNameOnly = Path.GetFileNameWithoutExtension(filePathAndName);
    string filePathWithoutFile = Path.GetDirectoryName(filePathAndName);
    string filename = filePathWithoutFile + "\\" + fileNameOnly + ".png";

    if (System.IO.File.Exists(filename))
    {
        return new Bitmap(filename);
    }
    else
    {
        List<Bitmap> bitmaps = new List<Bitmap>();
        int pageCount;

        using (Stream msTemp = new MemoryStream(tiffFile))
        {
            TiffBitmapDecoder decoder = new TiffBitmapDecoder(msTemp, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
            pageCount = decoder.Frames.Count;

            for (int i = 0; i < pageCount; i++)
            {
                System.Drawing.Bitmap bmpSingleFrame = Worker.BitmapFromSource(decoder.Frames[i]);
                bitmaps.Add(bmpSingleFrame);
            }

            Bitmap bmp = ImgHelper.MergeImagesTopToBottom(bitmaps);

            EncoderParameters eps = new EncoderParameters(1);
            eps.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 16L);
            ImageCodecInfo ici = Worker.GetEncoderInfo("image/png");

            bmp.Save(filename, ici, eps);

            return bmp;
        }
    }
}

Then I pass that list of bitmaps into a separate function to do the actual combining:

public static Bitmap MergeImagesTopToBottom(IEnumerable<Bitmap> images)
{
    var enumerable = images as IList<Bitmap> ?? images.ToList();
    var width = 0;
    var height = 0;

    foreach (var image in enumerable)
    {
        width = image.Width > width
            ? image.Width
            : width;
        height += image.Height;
    }

    Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format16bppGrayScale);                                      

    Graphics g = Graphics.FromImage(bitmap);//gives Out of Memory Exception

    var localHeight = 0;
    foreach (var image in enumerable)
    {
        g.DrawImage(image, 0, localHeight);
        localHeight += image.Height;
    }

    return bitmap;                     
}

But I usually get an Out of Memory Exception, depending on the number of frames in the tiff. Even just 5 images that are around 2550px by 3300px each are enough to cause the error. That's only around 42MB, which ends up saving as a png that is a total of 2,550px by 16,500px and is only 1.5MB on disk. I'm even using this setting in my web.config: <gcAllowVeryLargeObjects enabled=" true" /> Other details: I'm working on a 64bit Windows 7 with 16GB of RAM (and I normally run at around 65% of ram usage), and my code is running in Visual Studio 2013 in an asp.net MVC project. I'm running the project as 32 bit because I'm using Tessnet2 which is only available in 32 bit. Still, I figure I should have plenty of memory to handle much more than 5 images at a time. Is there a better way to go about this? I'd rather not have to resort to paid frameworks, if I can help it. I feel that this is something I should be able to do for free and with out-of-the-box .net code. Thanks!

Dylan West
  • 627
  • 10
  • 22
  • 2
    2550px by 3300px in 16bpp is 16,8 MB *each*. In addition, you are keeping the image you are composing into in memory too. So you need > 168 MB just for the image data (the fact that it compresses to 1.5 MB on disk is not relevant here). Still, this should be well within the limits of a 32 bit process (I guess 2 or 4 GB max memory), so not really an explanation... – Harald K Oct 18 '17 at 11:49
  • What is `TiffBitmapDecoder`? @haraldK an explanation that is not really an explanation... I like it! :) – VDWWD Oct 20 '17 at 16:53
  • Have you tried without specifying any `PixelFormat`? – jsanalytics Oct 21 '17 at 00:59
  • Without any `PixelFormat` I can get all the way up to 60 images X 3300 X 2550, in a windows 10 machine, 16GB RAM. – jsanalytics Oct 21 '17 at 01:07
  • There's no such thing as "no pixelformat". It'll either default to something, or throw an exception. – Nyerguds Nov 20 '17 at 15:26

2 Answers2

5

TL;DR

Documentation for Graphics.FromImage says: This method also throws an exception if the image has any of the following pixel formats: Undefined, DontCare, Format16bppArgb1555, Format16bppGrayScale. So, use another format or try another Image library (which is not built on top of GDI+, like Emgu CV)

DETAILED

You're getting OutOfMemoryException, but it has nothing to do with a lack of memory. It's just how Windows GDI+ is working and how it's wrapped in .NET. We can see that Graphics.FromImage calls GDI+:

int status = SafeNativeMethods.Gdip.GdipGetImageGraphicsContext(new HandleRef(image, image.nativeImage), out gdipNativeGraphics);

And if status is not OK, it will throw an exception:

internal static Exception StatusException(int status)
{
    Debug.Assert(status != Ok, "Throwing an exception for an 'Ok' return code");

    switch (status)
    {
        case GenericError: return new ExternalException(SR.GetString(SR.GdiplusGenericError), E_FAIL);
        case InvalidParameter: return new ArgumentException(SR.GetString(SR.GdiplusInvalidParameter));
        case OutOfMemory: return new OutOfMemoryException(SR.GetString(SR.GdiplusOutOfMemory));
        case ObjectBusy: return new InvalidOperationException(SR.GetString(SR.GdiplusObjectBusy));
        .....
     }
}

status in your scenario is equal to 3, so why it throws OutOfMemoryException.

To reproduce it I just tried to create Image from 16x16 Bitmap:

int w = 16;
int h = 16;
Bitmap bitmap = new Bitmap(w, h, PixelFormat.Format16bppGrayScale);
Graphics g = Graphics.FromImage(bitmap); // OutOfMemoryException here

I used windbg + SOSEX for .NET extension for analysis and here what I can see: WinDbg

Artavazd Balayan
  • 2,353
  • 1
  • 16
  • 25
  • Seems you got it. But I must say: Brilliant API... :-P There are easily 4 other status codes/exception types that might give sense in this case, `OutOfMemoryException` is not among those. – Harald K Oct 23 '17 at 12:11
  • 1
    Probably they are not fixing it due to tons of legacy code which might break. Now it's a feature, not a bug :D – Artavazd Balayan Oct 23 '17 at 13:21
  • Changing the PixelFormat away from Format16bppGrayScale did the trick. And emgu cv is a pretty good tool to know about. Thanks! – Dylan West Oct 24 '17 at 16:11
4

I do not have data to test with, so I can't test your code. However it seems clear that at least you can save memory in more than one obvious way here.

Make these changes:

First, don't read all bytes and use the MemoryStream: you're copying the whole file to memory, AND then creating the Decoder with BitmapCacheOption.Default, which should load the whole in-memory stream into the decoder yet again ...

Eliminate the tiffFile array. In the using statement, open a FileStream on the file; and create your decoder with BitmapCacheOption.None --- no in-memory store for the decoder.

Then you create a full list of BitMaps for every frame! Instead, get your target size by just iterating the frames on the Decoder (BitmapFrame has PixelWidth). Use this to create your target BitMap; and then iterate the frames and draw each one there: put each frame's BitMap in a using block and dispose each one right after you've drawn it to your target.

Move your local Bitmap bmp variable outside the using block so all of the prior gets freeable after you've created that BitMap. Then outside of the using block write that to your file.

It seems clear that you're making two whole copies of the whole image, plus making each BitMap for each frame, plus making the final BitMap ... That's a lot of brute-force copies. It's ALL inside the one using block, so nothing leaves memory until your finish writing the new file.

There may yet be a better way to pass Streams to decoders and make BitMaps with Streams also, that will not require dumping all of the bytes into memory.

public Bitmap SaveTiffAsTallPng(string filePathAndName) {
    string pngFilename = Path.ChangeExtension(filePathAndName), "png");
    if (System.IO.File.Exists(pngFilename))
        return new Bitmap(pngFilename);
    else {
        Bitmap pngBitmap;
        using (FileStream tiffFileStream = File.OpenRead(filePathAndName)) {
            TiffBitmapDecoder decoder
                    = new TiffBitmapDecoder(
                            tiffFileStream,
                            BitmapCreateOptions.PreservePixelFormat,
                            BitmapCacheOption.None);
            int pngWidth = 0;
            int pngHeight = 0;
            for (int i = 0; i < decoder.Frames.Count; ++i) {
                pngWidth = Math.Max(pngWidth, decoder.Frames[i].PixelWidth);
                pngHeight += decoder.Frames[i].PixelHeight;
            }
            bitmap = new Bitmap(pngWidth, pngHeight, PixelFormat.Format16bppGrayScale);
            using (Graphics g = Graphics.FromImage(pngBitmap)) {
                int y = 0;
                for (int i = 0; i < decoder.Frames.Count; ++i) {
                    using (Bitmap frameBitMap = Worker.BitmapFromSource(decoder.Frames[i])) {
                        g.DrawImage(frameBitMap, 0, y);
                        y += frameBitMap.Height;
                    }
                }
            }
        }
        EncoderParameters eps = new EncoderParameters(1);
        eps.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 16L);
        pngBitmap.Save(
                pngFilename,
                Worker.GetEncoderInfo("image/png"),
                eps);
        return pngBitmap;
    }
}
Steven Coco
  • 542
  • 6
  • 16
  • This is better on memory management than what I had, for sure. I had originally used more using statements but took them out because I wasn't sure if I was removing references too soon. Also, I see now that this works better as one function rather than two. – Dylan West Oct 24 '17 at 16:22