Friday, July 29, 2011

Silverlight Drawing Tool: Silver Draw Whiteboard with Undo , Redo and Save as JPEG

My requirement was to create a whiteboard where user can draw simple shapes  and also erase drawing. User can also undo and redo drawing as they do in MS paint. After completing drawing user can also save the drawing as Image.

I did not wanted to reinvent wheel so I tried to find good Silverlight tool which support basic drawing. I found some Silverlight tools which give me such basic drawing facility. Silver Draw (http://www.codeproject.com/KB/silverlight/silverdraw.aspx) seems more promising. Which give me all basic functionality with nice color picker also.  I can draw with Pen, Brush . Also I can draw line, rectangle and ellipse. This also give WPF duplex chatting and sharing facility. I did not need sharing facility so I omit this in my example.
image
Now It reduce my work and I only need to add eraser, undo , redo and save facility.

Eraser :

Eraser is simply drawing brush which used to draw ellipse with 25 width and height . Its color is same as background color so that user will think that it act as eraser.

case CurrentTool.EraseBrush:
{
HideVirtualLine();
var spot = toolHelper.CreateBrush(PrevPoint, cupt, 25);
(spot as Shape).StrokeThickness = 0;
(spot as Shape).Fill = new SolidColorBrush(Color.FromArgb(255, 255, 255, 255));
_canvas.Children.Add(spot);
tempHolder.Add(spot as Shape);
//AddToUndoShape(spot as Shape);

PrevPoint = cupt;
break;

}

Undo, Redo :

Removing Line, Rectangle and Ellipse is easy because after mouse over this shape is added in canvas. But as pencil and brush do not have defined shape so line is created on every Mouse Move. But When user click on undo button I like to remove all pencil stroke that user draws at a time with mouse over. So I maintain another list for keeping every stroke on mouse move.


public Point DrawOnMove(Point cupt)
{
switch (tool)
{
case CurrentTool.Brush:
{
…….


break;

}

case CurrentTool.EraseBrush:
{
………..


break;

}
case CurrentTool.Pencil:
{
var pen = toolHelper.CreatePen(PrevPoint, cupt);
ApplyAttributes(pen as Shape);
(pen as Shape).StrokeThickness = 3;
_canvas.Children.Add(pen);
tempHolder.Add(pen as Shape);
PrevPoint = cupt;
break;
}
default:
………
}
return cupt;
}

here tempHolder contains every pencil stroke on mouse movement. And when user finish drawing with Mouse Over this collection is added to a dictionary  so that after clicking on undo, redo button this collection  is Added/Remove at a time.

public Point DrawOnComplete(Point cupt)
{
switch (tool)
{
case CurrentTool.Pen:
{
var pen = toolHelper.CreatePen(PrevPoint, cupt);
ApplyAttributes(pen as Shape);
_canvas.Children.Add(pen);
PrevPoint = cupt;
AddToUndoShape(pen as Shape);
break;

}

case CurrentTool.Rectangle:
{
……….;
break;
}
case CurrentTool.Ellipse:
{
……..


break;
}
case CurrentTool.Brush:
case CurrentTool.EraseBrush:
case CurrentTool.Pencil:
{
var shp = toolHelper.CreatePen(PrevPoint, cupt);
List<Shape> UnshapeList = new List<Shape>();
tempHolder.ForEach(p => UnshapeList.Add(p));
UnShapeDrawingItems.Add(shp as Shape, UnshapeList);
tempHolder.Clear();
AddToUndoShape(shp as Shape);
}
break;
}



return cupt;
}


 
Here for Pen, Rectangle and Ellipse, Shape is added to canvas and this shape is added to Undolist so that User can undo this Shape. But for Brush, EraseBrush and Pencil list of mouse


movement is added to a Dictionary by creating a virtual Pen as Key of Dictionary. This virtual Pen actually represent total mouse movement of Pencil/ Brush and this Pen is added to UndoList. 

My undo and redo list size is 400. When user click on Undo button UndoShape() function is called and when user click on redo then RedoShape() is called.For top item as Line/Ellipse and Rectangle


when user click on undo then this shape is removed from canvas and added to redo list. But for Pencil/Brush shape all strokes made by user before mouse over is removed from canvas at a time.


And the virtual Pen key item is added to redo list. RedoShape() function is completely opposite to UndoShape().


public void UndoShape()
{
if (undoTop > 0)
{
undoTop--;
Shape shape = UndoList[undoTop];
if (shape is Line && UnShapeDrawingItems.ContainsKey(shape))
{
foreach (var unShapeDrawingItem in UnShapeDrawingItems[shape])
{
_canvas.Children.Remove(unShapeDrawingItem);
}
}
else
_canvas.Children.Remove(shape);
UndoList.RemoveAt(undoTop);

AddToRedoList(shape);
}
}
public void RedoShape()
{
if (redoTop > 0)
{
redoTop--;
Shape shape = redoList[redoTop];
if (shape is Line && UnShapeDrawingItems.ContainsKey(shape))
{
foreach (var unShapeDrawingItem in UnShapeDrawingItems[shape])
{
_canvas.Children.Add(unShapeDrawingItem);
}
}
else
_canvas.Children.Add(shape);
redoList.RemoveAt(redoTop);
AddToUndoShape(shape);
}
}

private void AddToRedoList(Shape shape)
{
if (redoTop >= 400)
{
RemoveRedoBottom();
}
redoTop++;
redoList.Add(shape);
}

private void RemoveRedoBottom()
{
redoTop--;
redoList.RemoveAt(0);
}

public void AddToUndoShape(Shape shape)
{
if (undoTop >= 400)
{
RemoveUndoBottom();
}
undoTop++;
UndoList.Add(shape);

}

private void RemoveUndoBottom()
{
undoTop--;
UndoList.RemoveAt(0);
}


Saving as Image:

Now I have given Erase and Undo , Redo facility to user. And user want to save this created drawing in their server. I found an example (http://www.andybeaulieu.com/silverlight/3.0/printablesilverlight/printablesilverlight.aspx)where canvas is saved as PNG in postback. But the size of PNG is more than 2MB. I do not need so high quality image and want to reduce its size. So for image I prefer JPEG. I used FJ.Core dll for JPEG encoding as give in this stackoverflow (http://stackoverflow.com/questions/1139200/using-fjcore-to-encode-silverlight-writeablebitmap) . I also have reduced the size of image with ImageResizer.


private static string GetBase64Jpg(WriteableBitmap bitmap)
{
int width = bitmap.PixelWidth;
int height = bitmap.PixelHeight;
int bands = 3;
byte[][,] raster = new byte[bands][,];

for (int i = 0; i < bands; i++)
{
raster[i] = new byte[width, height];
}

for (int row = 0; row < height; row++)
{
for (int column = 0; column < width; column++)
{
int pixel = bitmap.Pixels[width * row + column];
raster[0][column, row] = (byte)(pixel >> 16);
raster[1][column, row] = (byte)(pixel >> 8);
raster[2][column, row] = (byte)pixel;
}
}

ColorModel model = new ColorModel { colorspace = ColorSpace.RGB };
FluxJpeg.Core.Image img = new FluxJpeg.Core.Image(model, raster);
MemoryStream stream = new MemoryStream();
ImageResizer resizer = new ImageResizer(img);
var resizedImage =resizer.Resize(300, 300,ResamplingFilters.NearestNeighbor);
JpegEncoder encoder = new JpegEncoder(resizedImage, 90, stream);
encoder.Encode();

stream.Seek(0, SeekOrigin.Begin);
byte[] binaryData = new Byte[stream.Length];
long bytesRead = stream.Read(binaryData, 0, (int)stream.Length);

string base64String =
System.Convert.ToBase64String(binaryData,
0,
binaryData.Length);

return base64String;
}

Now this whiteboard become a complete with Erase, Undo/ Redo and Save facility. You can find modified Silver Draw code with these feature in

http://dl.dropbox.com/u/20275838/SilverlightClient.rar