Thursday, July 28, 2011

Problem of saving images in WPF (RenderTargetBitmap)

To save a visual to an image file need to use RenderTargetBitmap, detail is reference to Save and read images in WPF.

But sometimes you will find the output image was shifted or left blank. This is because that RenderTargetBitmap render the visual object based on cordinate of its parent object. Margin of itself, Padding or BorderThickness of its parent will all affect the rendered image. Although I think this is a bug of WPF, it seems the feature is by design as reference to RenderTargetBitmap layout offset influence.

There are three ways to fix the problem,
SolutionDescription
Add a BorderSimple, but the visual logical tree is changed.
Use a VisualBrushMaintain the original visaul logical tree, but need to do more process.
Temporary change the reative postion by Measure() and Arrange()Need to change back after rendering, and the two function is automatically called while repainting by WPF, so the real process is hard to tell.

The first solution is simplist. RenderTargetBitmap is only shifted by the distance between visaul and its parent object, so you just need to add a fake parent and move the margin to the parent object.

If the original visual logical tree is like

<Grid>
<Canvas Margin="20" />
</Grid>

changed to

<Grid>
<Border Margin="20">
<Canvas />
</Border>
</Grid>

and just call the method of SaveTo as Save and read images in WPF.

private void SaveTo(Visual v, string f)
{
/// get bound of the visual
Rect b = VisualTreeHelper.GetDescendantBounds(v);

/// new a RenderTargetBitmap with actual size of c
RenderTargetBitmap r = new RenderTargetBitmap(
(int)b.Width, (int)b.Height,
96, 96, PixelFormats.Pbgra32);

/// render visual
r.Render(v);

/// new a JpegBitmapEncoder and add r into it
JpegBitmapEncoder e = new JpegBitmapEncoder();
e.Frames.Add(BitmapFrame.Create(r));

/// new a FileStream to write the image file
FileStream s = new FileStream(f,
FileMode.OpenOrCreate, FileAccess.Write);
e.Save(s);
s.Close();
}

Second solution is draw the visaul to a DrawingVisual object, and pass the object to the SaveTo function.

private DrawingVisual ModifyToDrawingVisual(Visual v)
{
/// new a drawing visual and get its context
DrawingVisual dv = new DrawingVisual();
DrawingContext dc = dv.RenderOpen();

/// generate a visual brush by input, and paint
VisualBrush vb = new VisualBrush(v);
dc.DrawRectangle(vb, null, b);
dc.Close();

return dv;
}

PS. The context will act after calling Close(). You can use the using statement to block the region. And the SaveTo method should be modified as

/// render visual
r.Render(ModifyToDrawingVisual(v));


The third solution is to temporarily change the reative postion by Measure and Arrange before Render,

private void ModifyPosition(FrameworkElement fe)
{
/// get the size of the visual with margin
Size fs = new Size(
fe.ActualWidth +
fe.Margin.Left + fe.Margin.Right,
fe.ActualHeight +
fe.Margin.Top + fe.Margin.Bottom);

/// measure the visual with new size
fe.Measure(fs);

/// arrange the visual to align parent with (0,0)
fe.Arrange(new Rect(
-fe.Margin.Left, -fe.Margin.Top,
fs.Width, fs.Height));
}

PS. The solution is only suitable for UIElement, and need to change the position back after rendering.

private void ModifyPositionBack(FrameworkElement fe)
{
/// remeasure a size smaller than need, wpf will
/// rearrange it to the original position

fe.Measure(new Size());
}

Because the size to be measured is smaller then the real size, WPF will rearrange the layout and align the position back. And the render part is modified as

/// render visual
ModifyPosition(v as FrameworkElement);
r.Render(v);
ModifyPositionBack(v as FrameworkElement);

About Measure() and Arrange(), detail is reference to UIElement.Measure Method

--
Reference
RenderTargetBitmap layout offset influence
RenderTargetBitmap tips
RenderTargetBitmap and XamChart - Broken, Redux

6 comments:

  1. You rock!!! I've used the third solution, thanks!

    ReplyDelete
  2. Thank you for this solution :) This is all I need

    ReplyDelete
  3. @Egor: Which method r u using?? I tried to use first but it didnt work.

    ReplyDelete
    Replies
    1. I introduced a manual shift of my canvas and used the following code:
      private void ConvertChartToImage()
      {
      double[] chartOffset = new double[2];
      /// get the size of the visual with margin
      Size fs = new Size(
      canvas.ActualWidth +
      canvas.Margin.Left + canvas.Margin.Right,
      canvas.ActualHeight +
      canvas.Margin.Top + canvas.Margin.Bottom);

      /// measure the visual with new size
      canvas.Measure(fs);

      /// arrange the visual to align parent with (0,0)
      canvas.Arrange(new Rect(
      -canvas.Margin.Left + chartOffset[0], -canvas.Margin.Top + chartOffset[1],
      fs.Width, fs.Height));

      /// get bound of the visual
      Rect b = VisualTreeHelper.GetDescendantBounds(canvas);

      /// new a drawing visual and get its context
      DrawingVisual dv = new DrawingVisual();
      DrawingContext dc = dv.RenderOpen();

      /// generate a visual brush by input, and paint
      VisualBrush vb = new VisualBrush(canvas);
      dc.DrawRectangle(vb, null, b);
      dc.Close();

      /// new a RenderTargetBitmap with actual size of c
      RenderTargetBitmap r = new RenderTargetBitmap(
      (int)b.Width, (int)b.Height,
      96, 96, PixelFormats.Pbgra32);

      /// render visual
      r.Render(dv);

      /// new an image with decoded image
      Image i = new Image();
      i.Source = r;

      /// add the image into canvas
      canvas.Children.Clear();
      canvas.Children.Add(i);

      }

      If you have any questions about it, You may write to me at email: nuessence.music@gmail.com

      Cheers, Egor.

      Delete
  4. Even after 5 years after the reporting, its still there! Definitely a bug by my standards. Thanks for the solutions (used the 2nd).

    ReplyDelete
  5. Old Post, but wanted to leave this here in case it helps anyone.

    ModifyToDrawingVisual() method does not work as it's currently shown. Variable "b" is not defined. It's looking for a Rect object there, best option is to add the following line before using "b":

    Rect b = VisualTreeHelper.GetDescendantBounds(v);

    ReplyDelete