Out of Memory in Xamarin.Forms WPF while taking screenshots and saving as png

I am needing to save 200 screenshots (making a video) from a Xamarin.Forms WPF project.

I wrote the following in the WPF project to take a screenshot and save it as a .png file. It works fine, but when taking 200 successive shots (slight delay between each shot), I get an Out of Memory exception. I've read that there are some memory leaks in bitmaps with the Net Framework, but no idea where ...

I am calling the CaptureScreenShotAsync method via dependency service from the forms project.

public void CaptureScreenShotAsync(float left, float right, float top, float bottom, string filePath)
{
    BitmapSource fullBmpSource;
    using (var screenBmp = new Bitmap(
        (int)SystemParameters.PrimaryScreenWidth,
        (int)SystemParameters.PrimaryScreenHeight,
        PixelFormat.Format32bppArgb))
    {
        using (var bmpGraphics = Graphics.FromImage(screenBmp))
        {
            bmpGraphics.CopyFromScreen(0, 0, 0, 0, screenBmp.Size);
            fullBmpSource = Imaging.CreateBitmapSourceFromHBitmap(
                screenBmp.GetHbitmap(),
                IntPtr.Zero,
                Int32Rect.Empty,
                BitmapSizeOptions.FromEmptyOptions());
            fullBmpSource.Freeze();
        }
    }
    var fullWidth = fullBmpSource.PixelWidth;
    var fullHeight = fullBmpSource.PixelHeight;

    BitmapSource bmpSource;

    if (left.IsApproximatelyEqualTo(0f) && top.IsApproximatelyEqualTo(0f)
                                         && right.IsApproximatelyEqualTo(1f) && bottom.IsApproximatelyEqualTo(0f))
    {
        bmpSource = fullBmpSource;
    }
    else
    {
        var colStart = (int)(fullWidth * left);
        var colEnd = (int)(fullWidth * right);
        if ((colStart.IsEven() && colEnd.IsEven()) || (colStart.IsOdd() && colEnd.IsOdd()))
            colEnd = colEnd - 1;
        var rowStart = (int)(fullHeight * top);
        var rowEnd = (int)(fullHeight * bottom);
        if ((rowStart.IsEven() && rowEnd.IsEven()) || (rowStart.IsOdd() && rowEnd.IsOdd()))
            rowEnd = rowEnd - 1;
        var width = colEnd - colStart + 1;
        var height = rowEnd - rowStart + 1;
        bmpSource = new CroppedBitmap(fullBmpSource, new Int32Rect(colStart, rowStart, width, height));
        bmpSource.Freeze();
    }

    SaveBitmapAsFile(bmpSource, "PNG", filePath);

}

private static void SaveBitmapAsFile(BitmapSource bitmap, string format, string file)
{
    using (var fileStream = new FileStream(file, FileMode.Create))
    {
        BitmapEncoder encoder;
        switch (format)
        {
            case "BMP":
                encoder = new BmpBitmapEncoder();
                break;
            case "PNG":
                encoder = new PngBitmapEncoder();
                break;
            default:
                encoder = new PngBitmapEncoder();
                break;
        }
        encoder.Frames.Add(BitmapFrame.Create(bitmap));
        encoder.Save(fileStream);
    }
}
Tagged:

Best Answer

  • CaseCase US ✭✭✭
    Accepted Answer

    The call to GetHbitmap leaves an open handle which you have to dispose of manually.

    Solution is to wrap it in a MemoryStream

    private static BitmapSource GetScreenBitmapSource()
    {
        var source = Application.Current.MainWindow;
        if (source == null) { throw new Exception("Can't get screen"); };
        var renderWidth = source.RenderSize.Width;
        var renderHeight = source.RenderSize.Height;
    
        var ix = Convert.ToInt32(0);
        var iy = Convert.ToInt32(0);
        var iw = Convert.ToInt32(renderWidth);
        var ih = Convert.ToInt32(renderHeight);
    
        using (var bitmap = new Bitmap(iw, ih, PixelFormat.Format32bppArgb))
        using (var g = Graphics.FromImage(bitmap))
        {
            g.CopyFromScreen(ix, iy, ix, iy, new System.Drawing.Size(iw, ih), CopyPixelOperation.SourceCopy);
            using (var ms = new MemoryStream())
            {
                bitmap.Save(ms,ImageFormat.Bmp);
                ms.Seek(0, SeekOrigin.Begin);
                if (InvokeRequired)
                    return (BitmapSource)Application.Current.Dispatcher.Invoke(
                        new Func<Stream, BitmapSource>(CreateBitmapSourceFromBitmap),
                        DispatcherPriority.Normal,
                        ms);
                return CreateBitmapSourceFromBitmap(ms);
            }
        }
    }
    
    private static bool InvokeRequired => Dispatcher.CurrentDispatcher != Application.Current.Dispatcher;
    
    private static BitmapSource CreateBitmapSourceFromBitmap(Stream stream)
    {
        var bitmapDecoder = BitmapDecoder.Create(
            stream,
            BitmapCreateOptions.PreservePixelFormat,
            BitmapCacheOption.OnLoad);
        var writable = new WriteableBitmap(bitmapDecoder.Frames.Single());
        writable.Freeze();
        return writable;
    }
    

Answers

  • have you tried changing your platform target from x86 to x64?

    I can't really replicate this with what you have provided.

    Since the memory consumption is rising above 1.1 GB, I would assume that none of the screen captures are being disposed of after they are saved as files.

    Is your intention to append the files together while they are in memory or after they have been saved as files?

  • CaseCase USMember ✭✭✭
    Accepted Answer

    The call to GetHbitmap leaves an open handle which you have to dispose of manually.

    Solution is to wrap it in a MemoryStream

    private static BitmapSource GetScreenBitmapSource()
    {
        var source = Application.Current.MainWindow;
        if (source == null) { throw new Exception("Can't get screen"); };
        var renderWidth = source.RenderSize.Width;
        var renderHeight = source.RenderSize.Height;
    
        var ix = Convert.ToInt32(0);
        var iy = Convert.ToInt32(0);
        var iw = Convert.ToInt32(renderWidth);
        var ih = Convert.ToInt32(renderHeight);
    
        using (var bitmap = new Bitmap(iw, ih, PixelFormat.Format32bppArgb))
        using (var g = Graphics.FromImage(bitmap))
        {
            g.CopyFromScreen(ix, iy, ix, iy, new System.Drawing.Size(iw, ih), CopyPixelOperation.SourceCopy);
            using (var ms = new MemoryStream())
            {
                bitmap.Save(ms,ImageFormat.Bmp);
                ms.Seek(0, SeekOrigin.Begin);
                if (InvokeRequired)
                    return (BitmapSource)Application.Current.Dispatcher.Invoke(
                        new Func<Stream, BitmapSource>(CreateBitmapSourceFromBitmap),
                        DispatcherPriority.Normal,
                        ms);
                return CreateBitmapSourceFromBitmap(ms);
            }
        }
    }
    
    private static bool InvokeRequired => Dispatcher.CurrentDispatcher != Application.Current.Dispatcher;
    
    private static BitmapSource CreateBitmapSourceFromBitmap(Stream stream)
    {
        var bitmapDecoder = BitmapDecoder.Create(
            stream,
            BitmapCreateOptions.PreservePixelFormat,
            BitmapCacheOption.OnLoad);
        var writable = new WriteableBitmap(bitmapDecoder.Frames.Single());
        writable.Freeze();
        return writable;
    }
    
Sign In or Register to comment.