dotnet/wpf

WPF: CroppedBitmap crashes with AccessViolationException

vsfeedback opened this issue ยท 11 comments

This issue has been moved from a ticket on Developer Community.


[severity:I'm unable to use this version]
I'm trying to save a screenshot of a diagram I generated in my .NET 5 software.

The diagram in the WPF window consists of thousands of WPF elements and has a width of approx. 300,000*2500 pixels.

I'm using RenderTargetBitmap to create a bitmap from my WPF window content.

When doing so, ...

  1. PngBitmapEncoder throws an ArgumentException, claiming it cannot convert such large image.
  2. Splitting the image using CroppedBitmap in slices of 20,000*2500 pixels each results in an AccessViolationException.

Please refer to the step recorder information for further details.


Original Comments

Feedback Bot on 8/18/2021, 00:57 AM:

We have directed your feedback to the appropriate engineering team for further evaluation. The team will review the feedback and notify you about the next steps.


Original Solutions

(no solutions)

Here's a screencast depicting the issue:

AccessViolation in CroppedBitmap

... and here's the important pieces of my code:

private void SaveImage()
{
  SaveFileDialog fileDialog = new SaveFileDialog()
  {
    Filter = "PNG files|*.png",
    Title = "Save dependency diagram"
  };

  if (fileDialog.ShowDialog() ?? false)
    if (fileDialog.FileName is string filePath)
    {
      int resolution = _options.ImageResolutionDPI;
      double scaling = resolution / 96f;
      int width = (int)(ContentPresenter.ActualWidth * scaling), height = (int)(ContentPresenter.ActualHeight * scaling);

      RenderTargetBitmap bitmap = new RenderTargetBitmap(width, height, resolution, resolution, PixelFormats.Pbgra32);

      bitmap.Render(ContentPresenter);

      using FileStream imageFile = new FileStream(filePath, FileMode.Create, FileAccess.Write);
      BitmapEncoder png = new PngBitmapEncoder();
      png.Frames.Add(BitmapFrame.Create(bitmap));

      try
      {
        png.Save(imageFile);
      }
      catch (ArgumentException ex)
      {
        MessageBoxResult choice = MessageBox.Show($@"{ex.Message}

Image dimensions: {width}*{height} pixels.

Do you want to save the diagram by splitting the image into several files?", "Diagram image could not be saved", MessageBoxButton.YesNo, MessageBoxImage.Question);

        imageFile.Close();
        File.Delete(filePath);

        if (choice == MessageBoxResult.Yes)
          SaveTiledImage(bitmap, filePath);
      }
    }
}

/// <summary>
///		Splits the bitmap to be saved into tiles and
///		saves each of the tiles separately, adding a
///		sequence number to the tiles' file names.
/// </summary>
/// <param name="bitmap">
///		Bitmap to be saved to disk.
/// </param>
/// <param name="filePath">
///		Destination file path for the image to be saved.
/// </param>
private static void SaveTiledImage(RenderTargetBitmap bitmap, string filePath)
{
  SystemException? exception = null;
  string path = Path.GetDirectoryName(filePath)!;
  string newDir = $"{Path.GetFileNameWithoutExtension(filePath)} ({DateTime.Now:yyyy-MM-dd_hh;mm;ss})";
  string filePattern = Path.GetFileNameWithoutExtension(filePath)! + "-{0:D}" + Path.GetExtension(filePath);

  try
  {
    if (Directory.Exists(path))
    {
      path = Path.Combine(path, newDir);
      Directory.CreateDirectory(path);

      filePath = Path.Combine(path, filePattern);

      int startSteps = (int)bitmap.Width / 40000 * 2;

      for (int steps = startSteps; steps < startSteps + 4; steps += 2)
      {
        exception = null;

        for (int idx = 0; idx < steps; ++idx)
          try
          {
            SavePartialImage(bitmap, filePath, idx, steps);
          }
          catch (SystemException ex)
          {
            exception = ex;
            break;
          }

        if (exception == null)
        {
          MessageBox.Show("Diagram image successfully saved using tiles.", "Diagram image saved");
          return;
        }
      }
    }
  }
  catch (SystemException ex)
  {
    exception = ex;
  }

  MessageBox.Show(exception!.Message, "Diagram image could not be saved", MessageBoxButton.OK, MessageBoxImage.Error);
}

/// <summary>
///		Saves a section of the provided bitmap to disk.
/// </summary>
/// <param name="bitmap">
///		Bitmap to be saved to disk.
/// </param>
/// <param name="filePath">
///		<para>Destination file path for the image to be saved.</para>
///		<para>The file path must contain a placeholder item
///		("<c>{0}</c>").</para>
/// </param>
/// <param name="index">
///		Section index of the bitmap part to save to disk.
/// </param>
/// <param name="steps">
///		Number of sections the bitmap is split into.
/// </param>
private static void SavePartialImage(RenderTargetBitmap bitmap, string filePath, int index, int steps)
{
  int width = (int)bitmap.Width / steps;
  int left = width * index;

  CroppedBitmap crop = new CroppedBitmap(bitmap, new Int32Rect(left, 0, width + (index == steps - 1 ? 0 : 1), (int)bitmap.Height));

  using FileStream imageFile = new FileStream(string.Format(filePath, index + 1), FileMode.Create, FileAccess.Write);
  BitmapEncoder png = new PngBitmapEncoder();
  png.Frames.Add(BitmapFrame.Create(crop));

  try
  {
    png.Save(imageFile);
  }
  catch (ArgumentException)
  {
    imageFile.Close();
    File.Delete(filePath);

    throw;
  }
}

Steps to reproduce (above code-behind provided):

<Window>
  <ScrollViewer>
    <UniformGrid Columns="100" x:Name="ContentPresenter">
      <StackPanel Width="3000"/>
    </UniformGrid>
  </ScrollViewer>
</Window>

I noticed three different issues above:

  1. Large PNG files cannot be encoded/created by PngBitmapEncoder.
  2. Saving a PNG file larger than 20,000 pixels in width raises an AccessViolationException.
  3. The AccessViolationException cannot be caught. The try-catch construct I added to SaveTiledImage doesn't catch the exception. The program is just getting aborted without any chance for me to catch the exception.

This could be a security vulnerability in the end.

Thank you, @GeraudFabien, for clarifying on the AccessViolationException behaviour.

So, from my perspective, item (3) may be considered solved. Just item (1) and (2) left.

Are you sure you run in X64 (Don't forget to uncheck prefer 32bits). https://stackoverflow.com/questions/9358507/configure-wpf-client-to-run-64bit
There is no way a 32 app con manipulate an image this big. Since to manipulate it you must have ~2 time more ram than the image (You must be able to store the original in bitmap the data used to compress in PNG and the final image).

Sorry but i can't help you for that part. You may want to split you're image and try to merge and encode the images on the filesystem using stream. It will bypass the limit of ram (If it's the problem)

Good hint. I compiled using X64 now, but to no avail. Nothing has changed.

I don't see a memory issue. I'm using a workstation machine with plenty of RAM, plus Windows virtual memory activated (by default).

The ArgumentException thrown by the PngBitmapEncoder is arbitrarily raised by code in the WPF framework. According to Wikipedia, the PNG standard allows for 232 * 232 pixels images:

https://en.wikipedia.org/wiki/Portable_Network_Graphics#Critical_chunks

  • IHDR must be the first chunk; it contains (in this order) the image's width (4 bytes); height (4 bytes);
    [...]
  • IDAT contains the image, which may be split among multiple IDAT chunks. Such splitting increases filesize slightly, but makes it possible to generate a PNG in a streaming manner.

I created an MVCE repository here:

https://github.com/SetTrend/AccessViolationIssue-dotnet-wpf-issues-5125

@GeraudFabien: I added the HandleProcessCorruptedStateExceptionsAttribute to the method causing the access violation, but the exception still isn't getting caught.

So, issue item (1) seems to still be valid.

@SetTrend The HandleProcessCorruptedStateExceptionsAttribute will do nothing.

.NET Core: Even though this attribute exists in .NET Core, since the recovery from corrupted process state exceptions is not supported, this attribute is ignored. The CLR doesn't deliver corrupted process state exceptions to the managed code.

See https://docs.microsoft.com/en-us/dotnet/api/system.runtime.exceptionservices.handleprocesscorruptedstateexceptionsattribute?view=net-5.0&WT.mc_id=WD-MVP-5003260

Thanks for clarifying, @lindexi!

Trying to have this issue bubbling up.

See PR above. You can workaround it by splitting into smaller pieces to avoid the overflow. Your demo has width 310_000 and height 2000. The stride is 4*310_000=1_240_000. The bitmap copying happens line be line. For line 1731, you get offset of 1_240_000*1731=2_146_440_000, for line 1732 it is -2_147_287_296, the input buffer points before the bitmap data and you get AV.