In the TabletPC SDKs and in WPF it's very easy to take an ink image and save it to an image format - BMP, JPG, etc.
Then came Silverlight and the InkPresenter and naturally I wanted to do the same. But it wasn't so easy.
Silverlight itself isn't bogged down with that functionality. So you first need to get the XAML representation of the Ink and send it to a service where either the TabletPC SDK or the WPF APIs are available. Even then you are not home free because the Silverlight ink is not quite the same as either of the other two. So you then need to extract data from the XAML representation of the Silverlight ink and create a new object for whichever API you choose.
This was all done in Silverlight 1.0. I haven't pulled this into Silverlight 1.1/2.0 yet, but it should all be the same and you still have to do the conversion on the server side. THe only difference of course, is the javascript needs to be converted to .NET code on the client side.
Even then, there is still some more trickery because there is something strange with using the width from the Silverlight object and I spent hours just experimenting with getting the proportions to display properly in the image. I also spent a lot of time struggling with the colors because the javascript output of the color values doesn't line up with what WPF wants. You'll see in the code comments all of the conversions going on.
Once I had all of that worked out (and this represents hours of effort) there were still some issues. Luckily, Stefan Wick, who is the ultimate guru on this topic and finally has a blog - hoorah!, was able to set me straight (and trim some of my code down significantly).
I hadn't thought much of this nor, apparently had anyone else, until someone recently emailed asking me how I did it so that he can use it as part of a solution in a competition. (I hope that the requirements of the competition don't say anything about original work!), so I thought I would blog the steps.
1) Convert the InkPresenter data to XAML . This does two things. FIrst it enables you to serialize it and pass it to a web service and secondly, it is the lowest reasonable common denominator for sharing between different objects. This javascript code comes from Gavin Gear.
This javascript code reads through the StrokeCollection property of an InkPresenter and builds up a string of xml that is the XAML representation of the StrokeCollection. You could also take the resulting string and pass it to CreateFromXAML to recreate the Silverlight StrokeCollection object.
if (strokeCollection.Count>0) { var xaml = "<StrokeCollection>"; if (strokeCollection != null) { for (var i = 0; i < strokeCollection.Count; i++) { var stroke = strokeCollection.GetItem(i); if (stroke.Name>"") xaml += "<Stroke Name='" + stroke.Name + "'><Stroke.DrawingAttributes>"; else xaml += "<Stroke><Stroke.DrawingAttributes>"; xaml += "<DrawingAttributes "; xaml += "Color='" + BrowserColorConverter(stroke.DrawingAttributes.Color) + "' "; xaml += "OutlineColor='" + convertColorToHexString(stroke.DrawingAttributes.OutlineColor) + "' "; xaml += "Width='" + stroke.DrawingAttributes.Width + "' "; xaml += "Height='" + stroke.DrawingAttributes.Height + "' "; xaml += "/></Stroke.DrawingAttributes>"; xaml += "<Stroke.StylusPoints>"; for (var j = 0; j < stroke.StylusPoints.Count; j++) { var stylusPoint = stroke.StylusPoints.GetItem(j); xaml += "<StylusPoint X='" + roundToTwoDecimalPlaces(stylusPoint.X) + "' Y='" + roundToTwoDecimalPlaces(stylusPoint.Y) + "' />"; } xaml += "</Stroke.StylusPoints></Stroke>"; } } xaml += "</StrokeCollection>";
2) Pass this string to a web service method that will do the following to it
3) Create a WPF InkObject from the XAML. Now that I'm in the web service, I can use .NET code. Phew. Note that I did this before I knew how to use LINQ to XML so I struggled through XPath to get this. Watch for an upcoming MSDN Mag article that will have updated code.
private static StrokeCollection InkObjectfromXAML(XmlNode StrokeColl) { StrokeCollection objStrokes = new StrokeCollection(); XmlNodeList strokeElements = StrokeColl.SelectNodes("Stroke"); foreach (XmlNode strokeNodeElement in strokeElements) { //step 1: create a new stroke from the stylus point elements in the XAML XmlNodeList stylusPointElements = strokeNodeElement.SelectNodes("./Stroke.StylusPoints/StylusPoint"); XmlNode drawAttribs = strokeNodeElement.SelectSingleNode("./Stroke.DrawingAttributes"); //points node is sent to GetStrokePOints method to convert to a type //that can be used by the new stroke System.Windows.Input.StylusPointCollection strokeData = GetStrokePoints(stylusPointElements); Stroke newstroke = new Stroke(strokeData); //step 2: grab color metadata about stroke from the xaml //color is a hex value //the stroke object requires a System.Windows.Media.Color type //following code performs the conversion string mycolor = drawAttribs.FirstChild.Attributes["Color"].Value; System.Drawing.Color drwColor = System.Drawing.ColorTranslator.FromHtml(mycolor); //build the new color from the a,r,g,b values of the drawing.color System.Windows.Media.Color newColor = new System.Windows.Media.Color(); newColor.A = drwColor.A; newColor.R = drwColor.R; newColor.G = drwColor.G; newColor.B = drwColor.B; //Step 3: extract width data from xaml, convert to int int myIntWidth; bool parseSuccess = int.TryParse(drawAttribs.FirstChild.Attributes["Width"].Value, out myIntWidth); //Step 4: apply width & color to stroke //some really wierd unexplainable transformations that I had to get // around until the final images looked right. if (myIntWidth == 3) newstroke.DrawingAttributes.Width = 1.5; else newstroke.DrawingAttributes.Width = 2; newstroke.DrawingAttributes.Color = newColor; //Step 5: add stroke to the stroke collection objStrokes.Add(newstroke); } return objStrokes; }
//This method (called from the method above, is an abstraciton of some sample code from Microsoft private static System.Windows.Input.StylusPointCollection GetStrokePoints(XmlNodeList stylusPointElements) { System.Windows.Input.StylusPointCollection pointData = new System.Windows.Input.StylusPointCollection(); //The object requires HiMetric point values, create multiplier for conversion double pixelToHimetricMultiplier = (2540d / 96d) / 100; foreach (XmlNode stylusPointElement in stylusPointElements) { string xStr = stylusPointElement.Attributes["X"].Value; string yStr = stylusPointElement.Attributes["Y"].Value; //x and y are in pixels, we need to multiply them to get them into HIMETRIC //space, which is what the InkAnalyzerBase expects int xInHimetric = (int)(System.Convert.ToDouble(xStr) * pixelToHimetricMultiplier); int yInHimetric = (int)(System.Convert.ToDouble(yStr) * pixelToHimetricMultiplier); pointData.Add(new System.Windows.Input.StylusPoint(xInHimetric, yInHimetric)); } return pointData; } Now we have an inkObject that WPF will be happy with!
//This method (called from the method above, is an abstraciton of some sample code from Microsoft
private static System.Windows.Input.StylusPointCollection GetStrokePoints(XmlNodeList stylusPointElements) {
System.Windows.Input.StylusPointCollection pointData = new System.Windows.Input.StylusPointCollection(); //The object requires HiMetric point values, create multiplier for conversion double pixelToHimetricMultiplier = (2540d / 96d) / 100; foreach (XmlNode stylusPointElement in stylusPointElements) { string xStr = stylusPointElement.Attributes["X"].Value; string yStr = stylusPointElement.Attributes["Y"].Value; //x and y are in pixels, we need to multiply them to get them into HIMETRIC //space, which is what the InkAnalyzerBase expects int xInHimetric = (int)(System.Convert.ToDouble(xStr) * pixelToHimetricMultiplier); int yInHimetric = (int)(System.Convert.ToDouble(yStr) * pixelToHimetricMultiplier); pointData.Add(new System.Windows.Input.StylusPoint(xInHimetric, yInHimetric)); } return pointData; }
4) Convert WPF Ink to PNG format bytes This is with a BIG thanks to Stefan - we need to start a separate thread to do the conversion from WPF Ink ojbect to PNG. That conversion happens inside the thread. Also, thank to his deep understanding of the ink object, Stefan was able to accomplish in a much smaller amount of code what I had achieved in about 3 times as much code. I was definitely doing loop-dee-loops, but it was the best I could come up at the time.
Note that I am not saving to an actual file here, just creating the bytes because my goal was to store that in a database.
private static void ThreadforConverttoPNG() { Thread t = new Thread(new ThreadStart(ConverttoPNG)); t.SetApartmentState(ApartmentState.STA); // Start ThreadProc. Note that on a uniprocessor, the new // thread does not get any processor time until the main thread // is preempted or yields. Uncomment the Thread.Sleep that // follows t.Start() to see the difference. t.Start(); } private static void ConverttoPNG() { //I had originally achieved this with a LOT more code. This is Stefan's more trimmed down method //create temporary InkCanvas InkCanvas inkCanvas = new InkCanvas(); inkCanvas.Strokes = strokes; //render InkCanvas to a RenderBitmapTarget Rect rect = inkCanvas.Strokes.GetBounds(); RenderTargetBitmap rtb = new RenderTargetBitmap((int)rect.Right, (int)rect.Bottom, 96d, 96d, System.Windows.Media.PixelFormats.Default); rtb.Render(inkCanvas); //endcode as PNG BitmapEncoder pngEncoder = new PngBitmapEncoder(); pngEncoder.Frames.Add(BitmapFrame.Create(rtb)); //save to memory stream System.IO.MemoryStream ms = new System.IO.MemoryStream(); pngEncoder.Save(ms); ms.Close(); strokeBytes= ms.ToArray(); }
5) My next step was actually to store the bytes into a database. I wasn't actually saving out to a file. But to do that is simple. System.IO.File lets you create a new file on the fly from a byte array.
System.IO.File.WriteAllBytes("C:\\myfile.png",strokeBytes);
So, those are all of the pieces from converting a silverlight inkpresenter image to an image file.
Remember Me
See my speaking schedule for more events
User Group Leader
Hosted by:
Powered by: newtelligence dasBlog 2.0.7226.0
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.
© Copyright 2008, Julie Lerman
E-mail