WPF Diagramming. Saving you canvas to image, XPS document or raw Xaml.


Guess saving visuals is one of the most spoken topics today. Main formats I wanted to implement within my application were png, xps and pure xaml so this is what I’ll dwell on below.

Exporting canvas to PNG image

public void ExportToPng(Uri path, Canvas surface)
{
  if (path == null) return;

  // Save current canvas transform
  Transform transform = surface.LayoutTransform;
  // reset current transform (in case it is scaled or rotated)
  surface.LayoutTransform = null;

  // Get the size of canvas
  Size size = new Size(surface.Width, surface.Height);
  // Measure and arrange the surface
  // VERY IMPORTANT
  surface.Measure(size);
  surface.Arrange(new Rect(size));

  // Create a render bitmap and push the surface to it
  RenderTargetBitmap renderBitmap = 
    new RenderTargetBitmap(
      (int)size.Width, 
      (int)size.Height, 
      96d, 
      96d, 
      PixelFormats.Pbgra32);
  renderBitmap.Render(surface);

  // Create a file stream for saving image
  using (FileStream outStream = new FileStream(path.LocalPath, FileMode.Create))
  {
    // Use png encoder for our data
    PngBitmapEncoder encoder = new PngBitmapEncoder();
    // push the rendered bitmap to it
    encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
    // save the data to the stream
    encoder.Save(outStream);
  }

  // Restore previously saved layout
  surface.LayoutTransform = transform;
}

I’ve again tried to make the code self-explaining. You can try to use different encoders for saving the data so it’s up to you to look through what ones are available. The main trouble many guys come across is blank images as the canvas output or saving canvas programmatically without visualizing it. The key is measuring and arranging the surface before pushing it to render bitmap. As render bitmap does nothing with measuring and arranging elements in this case, this should be regarded mandatory for you. And of course you should remember the zooming/scaling and rotation matrices ;) the layout transformations will also be saved to the image so you should take care of that manually. In my case I cache the LatoutTransform and reset the original value right before measuring canvas.

Paying more attention you can see that bitmap encoder accepts a Visual so canvas is not the only element that can be passed ;) Going down the inheritance tree it is convenient to use the FrameworkElement or pure Visual. FrameworkElement contains Width and Height properties used by canvas.

Default set references provided by WPF application template is quite enough to implement this method. Of course you’ll have to resolve "using" section ;)

Exporting canvas to XPS document

Actually this was the first I’ve started implementing for my application. I like XPS format very much because it is easy to maintain and integrate. Here’s the quick snippet of exporting your canvas to XPS

public void Export(Uri path, Canvas surface)
{
  if (path == null) return;

  // Save current canvas transorm
  Transform transform = surface.LayoutTransform;
  // Temporarily reset the layout transform before saving
  surface.LayoutTransform = null;

  // Get the size of the canvas
  Size size = new Size(surface.Width, surface.Height);
  // Measure and arrange elements
  surface.Measure(size);
  surface.Arrange(new Rect(size));

  // Open new package
  Package package = Package.Open(path.LocalPath, FileMode.Create);
  // Create new xps document based on the package opened
  XpsDocument doc = new XpsDocument(package);
  // Create an instance of XpsDocumentWriter for the document
  XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(doc);
  // Write the canvas (as Visual) to the document
  writer.Write(surface);
  // Close document
  doc.Close();
  // Close package
  package.Close();

  // Restore previously saved layout
  surface.LayoutTransform = transform;
}

You will need two additional assemblies to be referenced from the GAC: "ReachFramework.dll" and "System.Printing.dll". Again I’d advice you to backup the original layout transformation regardless it’s state before processing canvas. You should definitely read more details towards System.IO.Packaging and XPS documents. For those who doesn’t know yet the XPS document is presented by zip container for some xml, fonts and binary data like images etc. You can freely rename XPS to ZIP, unpack it and look inside for better understanding when you’ll be looking though MSDN articles towards it.

I was really pleased to find out that 10 or 20 of my duplicated elements having the same image with content template loaded from the separate assembly wasn’t cloned. I had only one image in the XPS for each distinct type of element so the archive size was extremely small for my diagram.

Exporting canvas to the XAML

I’ll give you the most stupid sample that can be found everywhere

public void Export(Uri path, Canvas surface)
    {
      if (path == null) return;
      if (surface == null) return;

      string xaml = XamlWriter.Save(surface);
      File.WriteAllText(path.LocalPath, xaml);
    }

You get the xaml presentation of your canvas that can be later loaded with XamlReader. Be prepared to cuss MS guys a lot because it is rather difficult to adopt any application load anything except simple xamls into your canvas :) The worst thing is that it doesn’t support bindings/multibindings so you’ll have to resolve them somehow on document loading. The same is for events and there’s a list of things not supported that can be easily found at MSDN shipped with Visual Studio. I’ve looked through the Internet to find any good solution but there’s nothing I liked. Guess if it becomes a hot topic (and I’m sure it will become soon) a lot of good overrides will appear. I’m doing my own implementation but it’s in the early stage and cannot be brought to public.

Anyway there’s a lot of cases when the pure xaml will be quite enough for you.

Have a nice coding and investigations.

About these ads

6 thoughts on “WPF Diagramming. Saving you canvas to image, XPS document or raw Xaml.

  1. Hello, I realy like you blog!
     
    I noticed that
    // Restore previously saved layoutsurface.LayoutTransform = transform;
     
    is missing from Exporting canvas to XPS document
     
    regards

  2. Hi, I would like to create an XPS document containing a snapshot from one of my custom controls – so I’ve build canvas, add canvas to XPS fixed document and get exeption while adding my control to the canvas – because my control is already included in other visual and logical tree. Have you an idea how I can get a snapshot of my graphic control to use them in the canvas.Thanks in advance!

  3. Are you trying to add your custom element to another Canvas just for creating a snapshot? You could try rendering your custom control directly being present in another layout. Image rendering usually takes Visual so this is everything you are using for the UI actually. Another approach I could suggest is temporarily removing logical child (your control) from existing layout and adding to that you are trying to process but it can’t appear not so eficcient. Try playing with existing layout as render source.

  4. Nice post :)
    In the ExportToPng method i suggest you to apply these little modifications, to avoid an undesired black border around the output image, and to supports the cases when Width and Height are NaN:

    public static void ExportToPng(this FrameworkElement surface, Uri path)
    {
    if (path == null) return;
    // Save current canvas transform
    Transform transform = surface.LayoutTransform;
    Thickness oldMargin = surface.Margin;
    // reset current transform (in case it is scaled or rotated)
    surface.LayoutTransform = null;
    surface.Margin = new Thickness(0);
    // Get the size of canvas
    double w = surface.Width.CompareTo(double.NaN) == 0 ? surface.ActualWidth : surface.Width;
    double h = surface.Height.CompareTo(double.NaN) == 0 ? surface.ActualHeight : surface.Height;
    Size size = new Size(w, h);

    ….
    ….
    ….SNIP….
    ….

    // Restore previously saved layout
    surface.Margin = oldMargin;
    surface.LayoutTransform = transform;
    }

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s