Question

I've written a small "watermark" program to add a custom watermark to an image. There are two watermarks a white one and a black one. The watermark is always placed on the left bottom side of the image. I clone that region of the image to determine which watermark should be placed based on that spot (the black watermark on light regions and the white watermark on dark regions).

When I use the application (either in debug or normal) on my machine - no problem. All images are processed and watermarks are added on the correct location.

However, on the client machine, the program breaks on all images throwing a OutOfMemory Exception on the clone part.

I know, ussually, the OutOfMemory Exception will also be thrown when I specify a region out of bounds, but since the function is working like a charm on my machine I can't imagine that is the case. Besides that, the program doesn't break after a few tries, it breaks on all attempts to clone.

Doesn't matter if there is text (DrawString method) or not. It breaks on the Clone.

The images being processed are big, but not "huge" (6016 x 4000 pixels at most) but even with smaller images (3264 x 2448 pixels) the client will break.

Variables:

bmOriginal : original bitmap

processImage : original image (pictureBox) - bmOriginal is a bitmap copy of this

watermarkText : textbox for extra information below watermark

black and white : pictureboxes containing the watermark images

watermarkCombo : combobox for selecting automatic, white or black (automatic fails)

Code:

using (Graphics gWatermark = Graphics.FromImage(bmOriginal))
{
    gWatermark.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
    System.Drawing.SolidBrush drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Black);

    // position watermark - watermark should be 10% of the image height
    int watermarkHeight = (int)(processImage.Image.Height * 0.1);
    int watermarkPadding = (int)(watermarkHeight * 0.1); // not completely true, but asume watermark is square
    Rectangle watermarkArea = new Rectangle(watermarkPadding, processImage.Image.Height - (watermarkPadding + (watermarkText.Text.Length == 0 ? 0 : watermarkPadding) + watermarkHeight), watermarkHeight, watermarkHeight);

    // determine color watermark
    bmWatermark = (Bitmap)black.Image;
    if (watermarkCombo.SelectedIndex == 0)
    {
        using (Bitmap watermarkClone = bmOriginal.Clone(watermarkArea, bmOriginal.PixelFormat))
        {
            var pixels = Pixels(watermarkClone);
            if (pixels.Average((Func<Color, decimal>)Intensity) < 110) // human eye adoption; normal threshold should be 128
            {
                bmWatermark = (Bitmap)white.Image;
                drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.White);
            }
        }
    }
    else if (watermarkCombo.SelectedIndex == 1)
    {
        bmWatermark = (Bitmap)white.Image;
        drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.White);
    }

    // draw the watermark
    gWatermark.DrawImage(bmWatermark, watermarkArea.X, watermarkArea.Y, watermarkArea.Width, watermarkArea.Height);

    // draw the text (if needed)
    if (watermarkText.Text.Length > 0)
    {
        System.Drawing.Font drawFont = new System.Drawing.Font("Tahoma", (float)watermarkPadding);
        gWatermark.DrawString(watermarkText.Text, drawFont, drawBrush, watermarkPadding, bmOriginal.Height - (watermarkPadding * 2));
    }
}
bmOriginal.Save(System.IO.Path.Combine(diWatermarked.FullName, fileName), System.Drawing.Imaging.ImageFormat.Jpeg);

line of error: using (Bitmap watermarkClone = bmOriginal.Clone(watermarkArea, bmOriginal.PixelFormat))

Now the big question: how do I get rid of that OutOfMemory exception.. anyone an idea?

EDIT When I choose not to determine the color of the watermark automatically and just add a watermark (let's say the white one) the program functions normally. I've seen the stack trace in an error log (on the catch of the function I output the exception and -if any- inner exceptions).

I know a lot of the OOM exceptions using the Clone function occur when you specify a region out of bounds; but that is not the case here.

When I look at my memory when using the app in debug mode, I start at 5.36 Gb program started and a normalized 5.39 Gb (with a max spike of 5.42 Gb) when running the execution I mentioned, it's not guzzling memory like crazy.

The code I use the determine the average "color" (it's from someone on StackOverflow - I just copied that from some other answer, can't find the link though);

// functions used to determine watermark color
private static decimal ComponentAverage(decimal a, decimal b)
{
    return Math.Min(a, b) + Math.Abs(a - b) / 2M;
}
private static decimal Intensity(Color color)
{
    decimal result = color.A;
    result = ComponentAverage(result, color.R);
    result = ComponentAverage(result, color.G);
    result = ComponentAverage(result, color.B);
    return result;
}
private static IEnumerable<Color> Pixels(Bitmap bitmap)
{
    for (int x = 0; x < bitmap.Width; x++)
        for (int y = 0; y < bitmap.Height; y++)
            yield return bitmap.GetPixel(x, y);
}

SOURCE There is a test project here: http://hotpepper.nu/oomtestapp.zip

Was it helpful?

Solution

Leon, I've changed your code you've uploaded to not lock any resources. I've notices that if I keep your application open, I cannot delete the output folders because some files are in use. This usually means that you DID NOT release all file handles, basically it is always the last file.

I couldn't reproduce the out of memory issue on my computer before and after my changes, seems to be an issue with very big files maybe?

Ok anyways, I found that you use ImageBox to load the white and black resource and to load the image from disc. This is not needed at all, intead use the resources directly

Bitmap white = OomTestApp.Properties.Resources.white;
Bitmap black = OomTestApp.Properties.Resources.black;

Then to load an image from disc, simply use Bitmap.FromFile

I have added some lines to release your resources properly .Dispose where no using blocks are used.

And I also removed the Clone() call because it is absolutely not needed I think, because you are just computing the pixels of the original image and you do not draw something into that image at that point. So what was the need to have the image cloned?

Here is the full code (starting after you created the folders)

if (errors == 0)
{
this.Height = 323;
goButton.Enabled = false;
stopButton.Enabled = true;

Bitmap white = OomTestApp.Properties.Resources.white;
Bitmap black = OomTestApp.Properties.Resources.black;
Bitmap bmWatermark = black;
Bitmap processImage = null;


progressBar1.Maximum = filesToProcess.Count;

foreach (string handleFile in filesToProcess)
{
    string fileName = System.IO.Path.GetFileName(handleFile);
    fileNameLabel.Text = "File: " + System.IO.Path.GetFileName(handleFile);
    try
    {
        // create backup if checked
        if (diOriginal != null)
        {
            System.IO.File.Move(handleFile, System.IO.Path.Combine(diOriginal.FullName, fileName));
            processImage = (Bitmap)Bitmap.FromFile(System.IO.Path.Combine(diOriginal.FullName, fileName));
        }
        else
        {
            processImage = (Bitmap)Bitmap.FromFile(handleFile);
        }

        double aspectRatio = (double)processImage.Width / (double)processImage.Height;

        using (Graphics gWatermark = Graphics.FromImage(processImage))
        {
            gWatermark.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
            System.Drawing.SolidBrush drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Black);

            // position watermark - watermark should be 10% of the image height
            int watermarkHeight = (int)(processImage.Height * 0.1);
            int watermarkPadding = (int)(watermarkHeight * 0.1); // not completely true, but asume watermark is square
            // calculate rectangle. if there is extra text, add extra padding below
            Rectangle watermarkArea = new Rectangle(watermarkPadding, processImage.Height - (watermarkPadding + (watermarkText.Text.Length == 0 ? 0 : watermarkPadding) + watermarkHeight), watermarkHeight, watermarkHeight);

            // determine color watermark
            bmWatermark = black;
            if (watermarkCombo.SelectedIndex == 0)
            {
                /*using (Bitmap watermarkClone = processImage.Clone(watermarkArea, processImage.PixelFormat))
                {*/
                var pixels = Pixels(processImage);
                if (pixels.Average((Func<Color, decimal>)Intensity) < 110) // human eye adoption; normal threshold should be 128
                {
                    bmWatermark = white;
                    drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.White);
                }
                //}
            }
            else if (watermarkCombo.SelectedIndex == 1)
            {
                bmWatermark = white;
                drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.White);
            }

            // draw the watermark
            gWatermark.DrawImage(bmWatermark, watermarkArea.X, watermarkArea.Y, watermarkArea.Width, watermarkArea.Height);

            // draw the text (if needed)
            if (watermarkText.Text.Length > 0)
            {
                System.Drawing.Font drawFont = new System.Drawing.Font("Tahoma", (float)watermarkPadding);
                gWatermark.DrawString(watermarkText.Text, drawFont, drawBrush, watermarkPadding, processImage.Height - (watermarkPadding * 2));
                drawFont.Dispose();
            }
            // disposing resources
            drawBrush.Dispose();

        }
        // save the watermarked file
        processImage.Save(System.IO.Path.Combine(diWatermarked.FullName, fileName), System.Drawing.Imaging.ImageFormat.Jpeg);


        // stop button pressed?
        Application.DoEvents();
        if (stopProcess) break;

        // update exection progress
        progressBar1.Value++;
        percentLabel.Text = ((int)((progressBar1.Value * 100) / filesToProcess.Count)).ToString() + "%";
        fileCountLabel.Text = "File " + progressBar1.Value.ToString() + "/" + filesToProcess.Count.ToString();
    }
    catch (Exception ex)
    {
        try
        {
            using (System.IO.StreamWriter sw = new System.IO.StreamWriter(System.IO.Path.Combine(folderText.Text, "errorlog.txt"), true))
            {
                sw.WriteLine("File: " + fileName);
                while (ex != null)
                {
                    sw.WriteLine("Message: " + ex.Message);
                    sw.WriteLine(ex.StackTrace);
                    sw.WriteLine(ex.Source);
                    ex = ex.InnerException;
                }
                sw.WriteLine();
            }
        }
        catch
        {
            // nothing to do - it already failed
        }
        errors++;
    }
    finally
    {
        if (processImage != null) processImage.Dispose();
    }
}

// dispose resources
white.Dispose();
black.Dispose();
bmWatermark.Dispose();



if (!stopProcess)
{
    // set status to complete
    fileCountLabel.Text = "File " + filesToProcess.Count.ToString() + "/" + filesToProcess.Count.ToString();
    percentLabel.Text = "100%";
    fileNameLabel.Text = "Completed...";
}
else
{
    fileNameLabel.Text = "Aborted...";
}

fileNameLabel.Text += errors.ToString() + " error(s) encountered";

// defaults to screen
progressBar1.Value = progressBar1.Maximum;
stopProcess = false;
goButton.Enabled = true;
stopButton.Enabled = false;

OTHER TIPS

Well, I didn't read everything on the page, but I wonder if anyone mentioned the "stride" of the Bitmap? Basically, your bitmap has to be a multiple of 4 bytes. I had this problem taking apart a grid to make tiles. The last tile would error because the Bitmap was not evenly divisible by 4.

The stride is the width of a single row of pixels (a scan line), rounded up to a four-byte boundary. If the stride is positive, the bitmap is top-down. If the stride is negative, the bitmap is bottom-up. https://softwarebydefault.com/2013/04/11/bitmap-color-balance/

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top