2025-11-28 00:35:46 +09:00

698 lines
29 KiB
C#

//-------------------------------------------------------------------------
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// File: Serialization.cs
// Ink Serialization Application
//
// This program demonstrates how one can serialize and deserialize
// ink in various formats.
//
// The application represents a form with fields for inputting first
// name, last name and signature. It allows the user to save this
// data as pure ISF (Ink Serialized Format) XML using base64 encoded
// ISF or HTML which references GIF images "fortified" with ink. It
// is also possible to Load from the XML and ISF formats. The ISF
// save/load uses extended properties to store the first name and last
// name, whereas the XML and HTML save/load stores this information in
// custom attributes.
//
// This sample does not support loading from the HTML format, because
// HTML is not a format suitable for storing structured data, such as
// forms. Because the data is separated into multiple streams (name,
// signature, etc.,) a format which preserves this separation such as
// XML or another kind of database format is required.
//
// HTML is very useful in a flow editing environment, such as a word
// processing document. The HTML that is saved by this sample uses
// fortified GIFs. These GIFs have ISF embedded within them, which
// preserves the full fidelity of the ink. A word processing
// application could save a document containing multiple types of
// data, such as images, tables, formatted text and ink persisted
// in an HTML format. This HTML would render without a hitch in
// browsers which have no special abilities to understand ink.
// However, when loaded into an application that is ink-aware, such
// as another word processing application, the full fidelity of the
// original ink is available and can be rendered at very high quality,
// edited or used for recognition.
//
// See the "Ink Interoperability" whitepaper, in this SDK, for more
// information about persistence formats for ink.
//
// The features used are: Load method, Save method, and extended properties
//
//--------------------------------------------------------------------------
using System;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.Text;
using System.IO;
using System.Xml;
using System.Runtime.InteropServices;
// The Ink namespace, which contains the Tablet PC Platform API
using Microsoft.Ink;
namespace Microsoft.Samples.TabletPC.Serialization
{
public class SerializationForm : System.Windows.Forms.Form
{
#region Standard Template Code
/// Required designer variable.
private System.ComponentModel.Container components = null;
private System.Windows.Forms.MainMenu MenuBar;
private System.Windows.Forms.MenuItem FileMenu;
private System.Windows.Forms.MenuItem NewMenu;
private System.Windows.Forms.MenuItem OpenMenu;
private System.Windows.Forms.MenuItem SaveAsMenu;
private System.Windows.Forms.MenuItem SeperatorMenu;
private System.Windows.Forms.MenuItem ExitMenu;
private System.Windows.Forms.GroupBox Signature;
private System.Windows.Forms.TextBox FirstNameBox;
private System.Windows.Forms.TextBox LastNameBox;
private System.Windows.Forms.Label FirstNameLabel;
private System.Windows.Forms.Label LastNameLabel;
#endregion
// The one and only ink collector
private InkCollector ic = null;
// These strings identify the GUIDs for custom attributes
private Guid FirstName = new Guid ("{29B2E996-57B6-4fda-BFFD-F54DBA4C35F9}");
private Guid LastName = new Guid ("{8A448E7C-0F39-4cd9-BB52-71FDA3221674}");
// Constructor
public SerializationForm()
{
#region Standard Template Code
//
// Required for Windows Form Designer support
//
InitializeComponent();
#endregion
// Create the InkCollector and attach it to the signature GroupBox
ic = new InkCollector(Signature.Handle);
ic.Enabled = true;
}
#region Standard Template Code
/// Clean up any resources being used.
/// Windows Forms Designer template code
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#endregion
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.MenuBar = new System.Windows.Forms.MainMenu();
this.FileMenu = new System.Windows.Forms.MenuItem();
this.NewMenu = new System.Windows.Forms.MenuItem();
this.OpenMenu = new System.Windows.Forms.MenuItem();
this.SaveAsMenu = new System.Windows.Forms.MenuItem();
this.SeperatorMenu = new System.Windows.Forms.MenuItem();
this.ExitMenu = new System.Windows.Forms.MenuItem();
this.Signature = new System.Windows.Forms.GroupBox();
this.FirstNameBox = new System.Windows.Forms.TextBox();
this.LastNameBox = new System.Windows.Forms.TextBox();
this.FirstNameLabel = new System.Windows.Forms.Label();
this.LastNameLabel = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// MenuBar
//
this.MenuBar.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.FileMenu});
//
// FileMenu
//
this.FileMenu.Index = 0;
this.FileMenu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.NewMenu,
this.OpenMenu,
this.SaveAsMenu,
this.SeperatorMenu,
this.ExitMenu});
this.FileMenu.Text = "&File";
//
// NewMenu
//
this.NewMenu.Index = 0;
this.NewMenu.Shortcut = System.Windows.Forms.Shortcut.CtrlN;
this.NewMenu.Text = "&New";
this.NewMenu.Click += new System.EventHandler(this.NewMenu_Click);
//
// OpenMenu
//
this.OpenMenu.Index = 1;
this.OpenMenu.Shortcut = System.Windows.Forms.Shortcut.CtrlO;
this.OpenMenu.Text = "&Open...";
this.OpenMenu.Click += new System.EventHandler(this.OpenMenu_Click);
//
// SaveAsMenu
//
this.SaveAsMenu.Index = 2;
this.SaveAsMenu.Shortcut = System.Windows.Forms.Shortcut.CtrlS;
this.SaveAsMenu.Text = "Save &As...";
this.SaveAsMenu.Click += new System.EventHandler(this.SaveAsMenu_Click);
//
// SeperatorMenu
//
this.SeperatorMenu.Index = 3;
this.SeperatorMenu.Text = "-";
//
// ExitMenu
//
this.ExitMenu.Index = 4;
this.ExitMenu.Text = "E&xit";
this.ExitMenu.Click += new System.EventHandler(this.ExitMenu_Click);
//
// Signature
//
this.Signature.Anchor = (((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right);
this.Signature.Location = new System.Drawing.Point(8, 48);
this.Signature.Name = "Signature";
this.Signature.Size = new System.Drawing.Size(456, 67);
this.Signature.TabIndex = 1;
this.Signature.TabStop = false;
this.Signature.Text = "Signature";
//
// FirstNameBox
//
this.FirstNameBox.Location = new System.Drawing.Point(8, 24);
this.FirstNameBox.Name = "FirstNameBox";
this.FirstNameBox.Size = new System.Drawing.Size(200, 20);
this.FirstNameBox.TabIndex = 2;
this.FirstNameBox.Text = "";
//
// LastNameBox
//
this.LastNameBox.Anchor = ((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right);
this.LastNameBox.Location = new System.Drawing.Point(216, 24);
this.LastNameBox.Name = "LastNameBox";
this.LastNameBox.Size = new System.Drawing.Size(248, 20);
this.LastNameBox.TabIndex = 3;
this.LastNameBox.Text = "";
//
// FirstNameLabel
//
this.FirstNameLabel.Location = new System.Drawing.Point(8, 8);
this.FirstNameLabel.Name = "FirstNameLabel";
this.FirstNameLabel.Size = new System.Drawing.Size(100, 16);
this.FirstNameLabel.TabIndex = 4;
this.FirstNameLabel.Text = "First Name";
//
// LastNameLabel
//
this.LastNameLabel.Location = new System.Drawing.Point(216, 8);
this.LastNameLabel.Name = "LastNameLabel";
this.LastNameLabel.Size = new System.Drawing.Size(100, 16);
this.LastNameLabel.TabIndex = 5;
this.LastNameLabel.Text = "Last Name";
//
// SerializationForm
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(472, 121);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.LastNameLabel,
this.FirstNameLabel,
this.LastNameBox,
this.FirstNameBox,
this.Signature});
this.Menu = this.MenuBar;
this.Name = "SerializationForm";
this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide;
this.Text = "Ink Serialization Sample Application";
this.ResumeLayout(false);
}
#endregion
#region Standard Template Code
/// The main entry point for the application.
[STAThread]
static void Main()
{
Application.Run(new SerializationForm());
}
#endregion
// If the user hits the new menu, clear the document
private void NewMenu_Click(object sender, System.EventArgs e)
{
// Erase the ink
ic.Ink.DeleteStrokes();
Signature.Invalidate();
// Clear the text boxes
FirstNameBox.Text = "";
LastNameBox.Text = "";
}
// If the user hits the exit menu, exit the application
private void ExitMenu_Click(object sender, System.EventArgs e)
{
ic.Enabled = false;
this.Dispose();
}
// This function handles the Save As.. command. It allows the user
// to specify a filename and location, as well as the type of format
// to save in. It then calls the appropriate helper function.
private void SaveAsMenu_Click(object sender, System.EventArgs e)
{
/// Create the SaveFileDialog, which presents a standard Windows
/// Save dialog to the user.
SaveFileDialog saveDialog = new SaveFileDialog();
/// Set the filter to suggest our recommended extensions
saveDialog.Filter =
"Ink Serialized Format files (*.isf)|*.isf|XML files (*.xml)|*.xml|" +
"HTML files (*.htm)|*.htm";
/// If the dialog exits and the user didn't choose Cancel
if(saveDialog.ShowDialog() == DialogResult.OK)
{
/// Create a stream which will be used to save data to the output file
/// Attempt to Open the file with read/write permission
using (FileStream myStream = new FileStream(saveDialog.FileName, FileMode.Create, FileAccess.ReadWrite))
{
// Put the filename in a more canonical format
String filename = saveDialog.FileName.ToLower();
// Get a version of the filename without an extension
// This will be used for saving the associated image
String extensionlessFilename = Path.GetFileNameWithoutExtension(filename);
// Get the extension of the file
String extension = Path.GetExtension(filename);
// Use the extension to determine what form to save the data in
switch (extension)
{
case ".xml":
SaveXML(myStream);
break;
/// The two HTML cases require a filename for saving associated images
case ".htm": case ".html":
SaveHTML(myStream, extensionlessFilename);
break;
/// If unfamiliar with the extension, use ISF, the most "native format"
case ".isf": default:
SaveISF(myStream);
break;
}
}
} // End if user chose OK from dialog
}
// This function saves the form in ISF format.
// It uses ExtendedProperties to preserve the first and last names.
// ExtendedProperties are an easy way to store non-ink data within an
// ink object. In this case, there is no outer format which contains the
// ink, so the only place to store the names is within the ink object itself.
private void SaveISF(Stream s)
{
byte[] isf;
// This is the ink object which will be serialized
ExtendedProperties inkProperties = ic.Ink.ExtendedProperties;
// Store the name fields in the ink object
// These fields will roundtrip through the ISF format
// Ignore empty fields since strictly empty strings
// cannot be stored in ExtendedProperties.
if (FirstNameBox.Text.Length > 0)
{
inkProperties.Add(FirstName, FirstNameBox.Text);
}
if (LastNameBox.Text.Length > 0)
{
inkProperties.Add(LastName, LastNameBox.Text);
}
// Perform the serialization
isf = ic.Ink.Save(PersistenceFormat.InkSerializedFormat);
// If the first and last names were added as extended
// properties to the ink, remove them - these properties
// are only used for the save and there is no need to
// keep them around on the ink object.
if (inkProperties.DoesPropertyExist(FirstName))
{
inkProperties.Remove(FirstName);
}
if (inkProperties.DoesPropertyExist(LastName))
{
inkProperties.Remove(LastName);
}
// Write the ISF to the stream
s.Write(isf,0,isf.Length);
}
// This function saves the form in XML format.
// It uses the base64 encoded version of the ink, which is most suitable
// for representation in XML. The names are stored as XML fields, rather
// than custom properties, so that these properties can be most easily
// accessible if the XML is saved in a database.
private void SaveXML(Stream s)
{
// This object will encode our byte data to a UTF8 string
UTF8Encoding utf8 = new UTF8Encoding();
byte[] base64ISF_bytes;
string base64ISF_string;
// Create a new XmlTextWriter.
XmlTextWriter xwriter = new XmlTextWriter(s, System.Text.Encoding.UTF8);
// Write the beginning of the document including the
// document declaration.
xwriter.WriteStartDocument();
// Write the beginning of the "data" element. This is
// the opening tag to our data
xwriter.WriteStartElement("SerializationSampleData");
// Get the base64 encoded ISF
base64ISF_bytes = ic.Ink.Save(PersistenceFormat.Base64InkSerializedFormat);
// Ink.Save returns a null terminated byte array. The encoding of the null
// character generates a control sequence when it is UTF8 encoded. This
// sequence is invalid in XML. Therefore, it is necessary to remove the
// null character before UTF8 encoding the array.
// The following loop finds the index of the first non-null byte in the byte
// array returned by the Ink.Save method.
int countOfBytesToConvertIntoString = base64ISF_bytes.Length - 1;
for(; countOfBytesToConvertIntoString >= 0; --countOfBytesToConvertIntoString)
{
// Break the loop if the byte at the index is non-null.
if(0 != base64ISF_bytes[countOfBytesToConvertIntoString])
break;
}
// Convert the index into count by incrementing it.
countOfBytesToConvertIntoString++;
// Convert it to a String
base64ISF_string = utf8.GetString(base64ISF_bytes, 0, countOfBytesToConvertIntoString);
// Write the ISF containing node to the XML
xwriter.WriteElementString("Ink", base64ISF_string);
// Write the text date from the form
// Note that the names are stored as XML fields, rather
// than custom properties, so that these properties can
// be most easily accessible if the XML is saved in a database.
xwriter.WriteElementString("FirstName",FirstNameBox.Text);
xwriter.WriteElementString("LastName",LastNameBox.Text);
//End the "data" element.
xwriter.WriteEndElement();
//End the document
xwriter.WriteEndDocument();
//Close the xml document.
xwriter.Close();
}
// This function saves the form in HTML format.
// It also creates an ink fortified GIF which is referenced by the HTML.
// A fortified GIF is an image file that has ISF embedded inside it. To an
// ink-unaware application, it is simply an image, but to an ink-aware
// application, all of the original ink data is available.
//
// Please refer to the "Ink Interoperability" whitepaper for more information
// on fortified GIFs.
private void SaveHTML(Stream s, String nameBase)
{
// It is not possible to save an empty .gif, so it
// is first necessary to ensure that the ink has a bounding
// box (i.e. the ink is not empty). The bounding box is
// used for this check instead of checking whether the stroke
// count is zero, since the ink could still be empty if the
// strokes don't contain any points.
if (ic.Ink.Strokes.GetBoundingBox().IsEmpty)
{
MessageBox.Show("Unable to save empty ink in HTML persistence format.");
}
else
{
byte[] fortifiedGif = null;
String html;
byte[] htmlBytes;
// This object will encode our byte data to a UTF8 string
UTF8Encoding utf8 = new UTF8Encoding();
// Create a directory to store the fortified GIF which also contains ISF
// and open the file for writing
Directory.CreateDirectory(nameBase + "_files");
using (FileStream gifFile = File.OpenWrite(nameBase + "_files\\signature.gif"))
{
// Generate the fortified GIF represenation of the ink
fortifiedGif = ic.Ink.Save(PersistenceFormat.Gif);
// Write and close the gif file
gifFile.Write(fortifiedGif, 0, fortifiedGif.Length);
}
// Create the HTML output
// Note that the names are stored in HTML tags, rather
// than custom properties, so that these properties can
// be easily retrieved from the HTML.
html =
"<HTML><HEAD></HEAD><BODY>" +
"<P>First Name: " +
FirstNameBox.Text +
" &nbsp;&nbsp;&nbsp;Last Name: " +
LastNameBox.Text +
"</P>" +
"<P><IMG src='" +
nameBase + "_files\\signature.gif" +
"' /></P>" +
"</BODY></HTML>";
// Convert the HTML to a byte array for writing to the stream
htmlBytes = utf8.GetBytes(html);
// Write the HTML to the stream
s.Write(htmlBytes, 0, htmlBytes.Length);
}
}
// This function handles the Open... command. It will determine the type of ink which is being
// opened and call the appropriate helper routine.
private void OpenMenu_Click(object sender, System.EventArgs e)
{
/// Create the OpenFileDialog, which presents a standard Windows
/// dialog to the user.
OpenFileDialog openDialog = new OpenFileDialog();
/// Set the filter to suggest our recommended extensions
openDialog.Filter =
"All Known Files (*.isf; *.xml)|*.isf; *.xml|Ink Serialized Format files (*.isf)|*.isf|" +
"XML files (*.xml)|*.xml|All Files (*.*)|*.*";
/// If the dialog exits and the user didn't choose Cancel
if(openDialog.ShowDialog() == DialogResult.OK)
{
/// Create a stream which will be used to load data from the output file
/// Attempt to Open the file with read only permission
using( FileStream myStream = new FileStream( openDialog.FileName, FileMode.Open, FileAccess.Read ) )
{
// Put the filename in a more canonical format
String filename = openDialog.FileName.ToLower();
// Get the extension of the file
String extension = filename.Substring(filename.LastIndexOf('.'));
// verify that the user is not actively inking
if (!ic.CollectingInk)
{
// Use the extension to determine what form to save the data in
switch (extension)
{
case ".isf":
LoadISF(myStream);
break;
case ".xml":
LoadXML(myStream);
break;
// If unfamiliar with the extension, assume ISF, the most "native format"
default:
LoadISF(myStream);
break;
}
}
else
{
// If ink collection is enabled, the active ink cannot be changed
MessageBox.Show("Unable to load ink while actively inking. Please try again.",
"Serialization",
MessageBoxButtons.OK);
}
}
}
}
// This function will load ISF into the ink object.
// It will also pull the data stored in the object's extended properties and
// repopulate the text boxes.
private void LoadISF(Stream s)
{
Microsoft.Ink.Ink loadedInk = new Microsoft.Ink.Ink();
byte[] isfBytes = new byte[s.Length];
// read in the ISF
s.Read(isfBytes, 0, (int) s.Length);
// load the ink into a new ink object
// once an ink object has been "dirtied" it can never load ink again
loadedInk.Load(isfBytes);
// temporarily disable the ink collector and swap ink objects
// We checked the InkCollector.CollectingInk property to verify
// the user has stopped inking. Otherwise, Enabled will throw
// InvalidOperationException.
ic.Enabled = false;
ic.Ink = loadedInk;
ic.Enabled = true;
// Repaint the inkable region
Signature.Invalidate();
ExtendedProperties inkProperties = ic.Ink.ExtendedProperties;
// Get the raw data out of this stroke's extended
// properties list, using our previously defined
// Guid as a key to the extended property we want.
// Since the save method stored the first and last
// name information as extended properties, we can
// remove this information now that the load is complete.
if (inkProperties.DoesPropertyExist(FirstName))
{
FirstNameBox.Text = (String) inkProperties[FirstName].Data;
inkProperties.Remove(FirstName);
}
else
{
FirstNameBox.Text = String.Empty;
}
if (inkProperties.DoesPropertyExist(LastName))
{
LastNameBox.Text = (String) inkProperties[LastName].Data;
inkProperties.Remove(LastName);
}
else
{
LastNameBox.Text = String.Empty;
}
}
// This function will load XML into the ink object.
// It will repopulate the text boxes using the data stored in the XML.
private void LoadXML(Stream s)
{
// This object will encode our byte data to a UTF8 string
UTF8Encoding utf8 = new UTF8Encoding();
XmlDocument xd = new XmlDocument();
XmlNodeList nodes;
Microsoft.Ink.Ink loadedInk = new Microsoft.Ink.Ink();
// Load the XML data into an XMLDocument object
xd.Load(s);
// Get the data in the ink node
nodes = xd.GetElementsByTagName("Ink");
// load the ink into a new ink object
// once an ink object has been "dirtied" it can never load ink again
if (0 != nodes.Count)
{
loadedInk.Load(utf8.GetBytes(nodes[0].InnerXml));
}
// temporarily disable the ink collector and swap ink objects
// We checked the InkCollector.CollectingInk property to verify
// the user has stopped inking. Otherwise, Enabled will throw
// InvalidOperationException.
ic.Enabled = false;
ic.Ink = loadedInk;
ic.Enabled = true;
// Repaint the inkable region
Signature.Invalidate();
// Get the data in the FirstName node
nodes = xd.GetElementsByTagName("FirstName");
if (0 != nodes.Count)
{
FirstNameBox.Text = nodes[0].InnerXml;
}
else
{
FirstNameBox.Text = String.Empty;
}
// Get the data in the LastName node
nodes = xd.GetElementsByTagName("LastName");
if (0 != nodes.Count)
{
LastNameBox.Text = nodes[0].InnerXml;
}
else
{
LastNameBox.Text = String.Empty;
}
}
}
}