Contents

1.0 - Introduction
2.0 - Overview
3.0 - Ink to Bitmap conversion
4.0 - Ink to Base64 conversion
5.0 - Summary
6.0 - Notes

Download Solution (Visual Studio 2008): Solution_WPFInkToBitmap_07.zip (1.9 MB).
Download test files (ISF, bitmaps): Testfiles_WPFInkToBitmap_07.zip (46 kB).

Environment: .NET Framework 2.0, WPF, Windows Forms.


1.0 - Introduction

InkCanvas is an element that can be used to receive and display ink input by drawing strokes on the surface with a stylus or with a mouse [1]. Ink can be used on an TabletPC but also a standard PC with an externally connected tablet. Drawings and text are saved as Ink Serialized Format (ISF), a proprietary Microsoft binary format with extension .isf [5].
In some cases, however, it is useful to have Ink in a more general format for showing drawings without using the InkCanvas control, by example in a browser or in a document.
With the converter, described here, Ink is converted to bitmap or saved to image file (.bmp, .gif, .jpg, png, tif or .wmp). It also can convert Ink to Base64 for storage in regular XML. The source code of the converter can be downloaded.


2.0 - Overview

Ink as Ink Serialized Format (ISF) is an efficient file format with a small size, suitable for storage and distribution. However, if you want to use a copy from the Ink as image in a webpage or in documentation, you need a bitmap. A class was made with name 'InkToBitmap' to convert ISF to a Bitmap object and also with functions for saving the bitmap to image file. Storage of (small) images in XML is best done best with Base64 code, so some functions were added for converting Ink to and from Base64.

The class itself is a WPF / Windows Forms hybrid with the methods and user interface from the WPF namespaces but it uses the OpenFileDialog, SaveFileDialog and ColorDialog from the System.Windows.Forms namespace (you could use the Microsoft.Win32.OpenFileDialog and Microsoft.Win32.SaveFileDialog), see also [3].
The wrapper for testing the class is also a Windows Forms application.




Fig. 2.1 - The converter can be used with or without a user interface.


After opening the interface it shows an editable surface for drawing strokes with a mouse or with a stylus on a tablet.

Menu items Open, Save and Save As are for files in ISF format. In the Tools menu are items for selecting strokes and for erasing points and strokes. A selected stroke can be resized, moved or deleted (key Delete). These tools are only intended for making small corrections. This is not a full blown editor.

In the Tools menu is also an option for clearing the canvas and for choosing a different background and pen color. The background color is not saved in the ISF, it is only saved in the bitmap (hence the Tools | Background Color menu).

With menu File | Close or with Tools | Clear Canvas the canvas is cleared and ready for drawing.

The converter functions are available as public methods in a single class for accessing them from code. The optional user interface (Fig. 2.1), which is also in the class, is opened with method ShowDialog.




Fig. 2.2 - The basic structure of the converter. The Ink from the InkCanvas control acts as an intermediate in all the processes.


  Table 1 - Properties in the converter class.
PropertyDescription
InkCanvas Get/Set the InkCanvas Control
BrushBack Get/Set the InkCanvas background color


When the class is instantiated, an InkCanvas control is created with default properties. Use property InkCanvas to change them. Property BrushBack is a shortcut for InkCanvas.Background.


  Table 2 - Methods in the converter class.
MethodDescription
OpenInkAsISF() Open Ink from ISF file (.isf)
SaveInkAsISF() Save Ink to ISF file (.isf)
SaveInkAsBitmap() Save Ink to image file (.bmp, .gif, .jpg, png, tif or .wmp)
OpenInkAsBase64() Open Ink as Base64 text from file (.txt)
SaveInkAsBase64() Save Ink as Base64 text to file (.txt)
StrokesToBitmap() Convert Ink to Bitmap (RenderTargetBitmap)
StrokesToBase64() Convert Ink to Base64 text
Base64ToStrokes() Convert a Base64 text to Ink
ShowDialog() Show the converter with a user interface


The Ink (StrokeCollection) is converted to bitmap (RenderTargetBitmap) and/or saved to a file with format .bmp, .gif, .jpg, png, tif or .wmp [4]. Saving to Image file or Bitmap Object is a one-way route, ISF can be used as the default file format for storing the Ink.

Ink saved as Base64 code is plain text. There are two universal functions StrokesToBase64() and Base64ToStrokes() for converting a StrokeCollection to and from Base64. The background color and size of the InkCanvas control are not saved with the code.
Base64 is suitable for adding it to XML elements in the same way as you would do for normal text.


3.0 - Ink to Bitmap conversion

InkCanvas is one of the controls in the System.Windows.Controls namespace (WPF) [1]. Other WPF namespaces used are System.Windows.Ink, System.Windows.Media and System.Windows.Media.Imaging. The Microsoft.Ink Namespace is not used, this namespace is for implementing Ink on tablet PC. As a consequence, the PersistenceFormat Enumeration cannot be used to save Ink as Base64 code (Base64InkSerializedFormat) and we must create our own functions for doing this.

Ink from the InkCanvas control is available as a collection of Stroke objects representing the lines in the drawing. In each Stroke is a collection of points, the StylusPoints. The StrokeCollection is the central object for all the functions in the class, it serves as an intermediate for all the conversions. The StrokeCollection can be retrieved with the InkCanvas.Strokes Method.

For converting Ink (the InkCanvas.StrokeCollection) to Bitmap we use the RenderTargetBitmap constructor [2]. It has parameters for the pixel format and the resolution (dpi) of the bitmap to create.




Fig. 3.1 - A fuzzy inked version of Fig. 2.2 drawn on the canvas in the user interface (with a tablet) and then saved as png (Ink_05.png).


This is the SaveInkAsBitmap() function from the class for saving Ink to an Image file:


  C# - StrokeCollection to bitmap converter

  using System;
  using System.IO;
  using System.Windows;
  using System.Windows.Media;
  using System.Windows.Media.Imaging; 
  [...]

  private System.Windows.Controls.InkCanvas m_inkCanvas;
  [...]

  // ---------------------------------------------------------------
  // Date      211007
  // Purpose   Public Method - Save the InkCanvas StrokeCollection to file as ISF.
  // Entry     oBB - Current brush for InkCanvas backgound color. 
  //           Use Property BrushBack.
  //           sFileName_out - Filename to save (+ path).
  // Return    The saved bitmap saved, null if not successful.
  // Comments  1) Uses m_inkCanvas, the InkCanvas control.
  //           2) The image is saved with current backgound color. 
  // ---------------------------------------------------------------
  public bool SaveInkAsBitmap(SolidColorBrush oBB, string sFileName_out)
  {
      try
      {
          // 1) Use bounding rectangle from strokes as outer size of image.
          Rect recBounds = m_inkCanvas.Strokes.GetBounds();

          /*
          // 2) Use size of canvas. Depends on user resizing window in GUI.
          Rect recBounds = new Rect();
          recBounds.X = 0;
          recBounds.Y = 0;
          recBounds.Width = m_inkCanvas.Width;
          recBounds.Height = m_inkCanvas.Height;
          */

          DrawingVisual dVisual = new DrawingVisual();
          DrawingContext dContext = dVisual.RenderOpen();
          dContext.PushTransform(new TranslateTransform(-recBounds.X, -recBounds.Y));

          // Default background color is transparent.
          // Use white when the file format does not support transparency.
          if (oBB == null)
          {
              oBB = Brushes.White;
          }
          dContext.DrawRectangle(oBB, null, recBounds);
          m_inkCanvas.Strokes.Draw(dContext);
          dContext.Close();

          RenderTargetBitmap oBitmap =
              new RenderTargetBitmap((int)recBounds.Width,
              (int)recBounds.Height,
              96, 96, PixelFormats.Default);
          oBitmap.Render(dVisual);

          // Use bitmapencoder depending on file-extension from user.
          BitmapEncoder oEncoder = null;
          switch (Path.GetExtension(sFileName_out))
          {
              case ".bmp":
                  oEncoder = new BmpBitmapEncoder();
                  iBitmapSelect = 1;
                  break;
              case ".jpg":
                  oEncoder = new JpegBitmapEncoder();
                  iBitmapSelect = 2;
                  break;
              case ".png":
                  oEncoder = new PngBitmapEncoder();
                  iBitmapSelect = 3;
                  break;
              case ".gif":
                  oEncoder = new GifBitmapEncoder();
                  iBitmapSelect = 4;
                  break;
              case ".tif":
                  oEncoder = new TiffBitmapEncoder();
                  iBitmapSelect = 5;
                  break;
              case ".wmp":
                  oEncoder = new WmpBitmapEncoder();
                  iBitmapSelect = 6;
                  break;
              default:
                  iBitmapSelect = 3;
                  // sRet = "This file extension is not supported.";
                  break;
          }

          if (oEncoder != null)
          {
              oEncoder.Frames.Add(BitmapFrame.Create(oBitmap));
              if (File.Exists(sFileName_out) == true)
              {
                  File.SetAttributes(sFileName_out, FileAttributes.Normal);
              }
              using (FileStream fs = File.Create(sFileName_out))
              {
                  oEncoder.Save(fs);
              }
          }
          return true;
      }
      catch
      {
          return false;
      }
  }


Most variables for this function are set to a default value, only the filename and the background color are implemented as parameter. The file format for the image file is extracted from the filename.

These are the key parameters for the converter, they can be used to make other versions of the function with SaveInkAsBitmap() as base:
  1. The size of the bitmap
  2. The background color (solid color or transparent)
  3. The pixel format for the bitmap
  4. The resolution of the bitmap (dpi)
  5. The file format (.bmp, .jpg, .png ... )
  6. Filename of the bitmap to save
The bitmap requires certain dimensions. There are different ways, e.g. with the dimensions of the InkCanvas (Width, Height) or with the dimensions from the virtual rectangle which includes all the Strokes (GetBounds method). The latter is done above.

Notice that, when the InkCanvas is resized with the window from the user interface, the bitmap is still saved with the size of this minimum rectangle. When a resizable image is needed (from the user interface) use the Width and Height properties of the InkCanvas.

Function SaveInkAsBitmap is used as follows from the File | Save As menu:


  C# - Ink to Bitmap (image file).

  private System.Windows.Media.SolidColorBrush m_brushBack;
  m_brushBack = Brushes.White;
  [...]
  int iBitmapSelect = 3;	// png.
  [...]

  // Menu File - Save Ink as Bitmap.
  private void mnuFileSaveAsBitmap_Click(object sender, RoutedEventArgs e)
  {
      SaveFileDialog saveFileDialog1 = new SaveFileDialog();
      saveFileDialog1.Filter =
          "Windows Bitmap (*.bmp)|*.bmp|" +
          "JPEG (*.jpg)|*.jpg|" +
          "Portable Network Graphics (*.png)|*.png|" +
          "CompuServe Graphics Interchange (*.gif)|*.gif|" +
          "Tagged Image File Format (*.tif)|*.tif|" +
          "Windows Media Photo (*.wmp)|*.wmp";
      saveFileDialog1.FilterIndex = iBitmapSelect;    // 1-based.
      if (saveFileDialog1.ShowDialog() == DialogResult.OK)
      {
          bool bRet = SaveInkAsBitmap(m_brushBack, saveFileDialog1.FileName);
          if (bRet == false)
          {
              System.Windows.MessageBox.Show("Error saving Ink as Bitmap.", "Error");
          }
      }
      saveFileDialog1.Dispose();
  }


In the class is also a function StrokesToBitmap() for creating a Bitmap (RenderTargetBitmap) from a StrokeCollection. This function has the same code as SaveInkAsBitmap() but it does not save to file. In the download is an example how to show a bitmap as RenderTargetBitmap in an Image control (System.Windows.Controls).


4.0 - Ink to Base64 conversion

When working with XML, a universal method for storing all sorts of data in elements is to have it in Base64 format. An example is the .resx resource file.
Converting Ink to Base64 code can be done with the Ink.Save method using the Base64InkSerializedFormat (PersistenceFormat enumeration) from the Microsoft.Ink namespace but this namespace is only for implementing digital ink on the Tablet PC.

As a stand-in, we can use a universal method instead: get the ink data as byte-array and convert the array to Base64 with the System.Convert.ToBase64String() method.


  C# - Ink Strokes to Base64.

  using System;
  using System.IO;
  [...]

  // ---------------------------------------------------------------
  // Date      300509
  // Purpose   Public Method - Convert a StrokeCollection to a Base64 text.
  // Entry     inkStrokes - A collection of Strokes, use m_inkCanvas.Strokes.
  // Return    The StrokeCollection as Base64 text.
  // Comments  1) Background color is not saved.
  //           2) The Base64 code is returned in lines with max 76 characters each.
  // ---------------------------------------------------------------
  public string StrokesToBase64(StrokeCollection inkStrokes)
  {
      try
      {
          using (MemoryStream ms = new MemoryStream())
          {
              // Save strokes from InkCanvas to a stream as ISF.
              inkStrokes.Save(ms);
              // Convert bytes from stream to Base64 text.
              byte[] isfBytes = ms.ToArray();
              return Convert.ToBase64String(isfBytes, 
                     Base64FormattingOptions.InsertLineBreaks);
          }
      }
      catch
      {
          return "";
      }
  }


The strokes are saved to MemoryStream to get the data as byte array. With the Convert.ToBase64String method the data from the array is converted to Base64 code. This method also allows us to 'fold' the code in lines with 76 characters each, which gives more readable code (in XML). Notice that the Base64 data is still in ISF format, only the appearance is changed.

Ink strokes converted to bitmap and Base64 code:




Fig. 4.1 - Ink converted to bitmap (Ink_01.png).


  Text - Ink converted to Base64 code (Ink_01.txt).
  
  AIIDAxdIEUTfvIIFRYQBGwIAJAFGhAEbAgAkAREAAIA/HwkRAAAAAAAA8D8KKRCH8ElHgjzms1mc
  0mQtEymc1gEBh/CkN4Uu7Nh7Dlos9rw6tKawCazIChgHh/BYJ4LOZkTKAIfwk7eE7W0TOzYetAAK
  NRSH8HRHg5TmqawCZTW1WaaTKZ4fmaZzIIfwlweEuHDmHsOTLDev7PonDrDmht5WiA5Rw7awCikQ
  h/Cxd4WLy0pnZpoyktFptM1Ah/B3l4OiVm0VoRoXgKcO5Tw6ylZpsAoaCIfwwFeGD+a2hMrVa4fw
  pDeE32YQC04fw6AKFQaH8N4XhvDmk1CH8KyHhUpmMBgFrAojDIfweueEJu0Ye0NwEmVMP6J0LayH
  8O6nhzNs+H7TaWHLVNQKWCmH8OMXhwvw9aEBs+HYDNGH7PMkzmRNJlaLNAJpa7TALXM4BAZlAJlN
  ZpM5kIfwsXeFi9NYAgMAmRNYBAZmtFpmkymcyw4mc0tczmkwxBa1ngMzmkyTWzg=


A second function StrokesFromBase64() converts the Base64 code back to a StrokeCollection:


  C# - Base64 to Ink Strokes converter.

  using System;
  using System.IO;
  [...]

  // ---------------------------------------------------------------
  // Date      300509
  // Purpose   Public Method - Convert a Base64 text to a StrokeCollection.
  // Entry     sBase64 - The Base64 text.
  // Return    A StrokeCollection. Returns null when not successful.
  // Comments  1) Use StrokesToBase64() to make a StrokeCollection as Base64.
  //           2) Use InkCanvas.Strokes = StrokesFromBase64(Base64-text)
  //           for showing the strokes in the InkCanvas Control.
  // ---------------------------------------------------------------
  public StrokeCollection Base64ToStrokes(string sBase64)
  {
      try
      {
          StrokeCollection sc = null;
          // Base64 to byte-array.
          byte[] isfBytes = Convert.FromBase64String(sBase64);
          // Save to a stream.
          using (MemoryStream ms = new MemoryStream())
          {
              ms.Write(isfBytes, 0, isfBytes.Length);
              ms.Position = 0;     // Wind back.
              // Get StrokeCollection from stream.
              sc = new StrokeCollection(ms);
          }
          return sc;
      }
      catch
      {
          return null;
      }
  }


It makes no difference if the Base64 code is folded into lines with 76 characters or not, the Convert.FromBase64String() function adopts to the format offered.

The returned StrokesCollection is shown in the InkCanvas control with:


  C# - Show Base64 as StrokeCollection in InkCanvas Control

 InkCanvas m_inkCanvas = new InkCanvas();
 string sBase64;  // Contains StrokeCollection as Base64.
 [...]
 StrokeCollection sc = StrokesFromBase64(sBase64);
 m_inkCanvas.Strokes = sc;


These two functions are available in the user interface from menu File | Open Ink As Base64 and File | Save Ink As Base64.
For the complete code, see the download.


5.0 - Summary

Ink from an InkCanvas control is usually saved in ISF format (Ink Serialized Format).
The described converter, wrapped in a class, is able to convert Ink from ISF to Bitmap object and/or save it to file in different image formats (.bmp, .gif, .jpg, .png, .gif, .tif and .wmp).
It is also possible to convert Ink to and from Base64 code for storage in resource files or XML.
The converter comes with an optional user interface for opening and saving Ink in these formats and it has some basic tools for editing the Ink on screen.


6.0 - Notes


[1] MSDN - InkCanvas Class.
Defines an area that receives and displays ink strokes.
http://msdn.microsoft.com/en-us/library/system.windows.controls.inkcanvas.aspx

[2] MSDN - RenderTargetBitmap Class.
Converts a Visual object into a bitmap.
http://msdn.microsoft.com/en-us/library/system.windows.media.imaging.rendertargetbitmap.aspx

[3] Windows Presentation Foundation SDK.
Uncommon Dialogs: Font Chooser & Color Picker Dialogs.
http://blogs.msdn.com/wpfsdk/archive/2006/10/26/Uncommon-Dialogs--Font-Chooser-and-Color-Picker-Dialogs.aspx

[4] HD Photo.
The HD Photo file format is also known as Windows Media Photo (.wdp, .wmp).
http://www.microsoft.com/windows/windowsmedia/forpros/wmphoto/

[5] Ink Serialized Format (ISF) Specification.
http://download.microsoft.com/download/0/B/E/0BE8BDD7-E5E8-422A-ABFD-4342ED7AD886/InkSerializedFormat(ISF)Specification.pdf