using System; using System.IO; using System.Net; using System.Xml; using System.Reflection; using System.Windows.Forms; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using SharpVectors.Xml; using SharpVectors.Dom; using SharpVectors.Dom.Css; using SharpVectors.Dom.Svg; using SharpVectors.Dom.Events; namespace SharpVectors.Renderers.Gdi { /// /// Renders a Svg image to GDI+ /// public sealed class GdiGraphicsRenderer : ISvgRenderer, IDisposable { #region Private Fields /// /// A counter that tracks the next hit color. /// private int counter; /// /// Maps a 'hit color' to a graphics node. /// /// /// The 'hit color' is an integer identifier that identifies the /// graphics node that drew it. When 'hit colors' are drawn onto /// a bitmap (ie. idMapRaster the 'hit color' /// of each pixel with the help of graphicsNodes can identify for a given x, y coordinate the /// relevant graphics node a mouse event should be dispatched to. /// private Dictionary graphicsNodes = new Dictionary(); /// /// The bitmap containing the rendered Svg image. /// private Bitmap rasterImage; /// /// A secondary back-buffer used for invalidation repaints. The invalidRect will /// be bitblt to the rasterImage front buffer /// private Bitmap invalidatedRasterImage; /// /// A bitmap image that consists of 'hit color' instead of visual /// color. A 'hit color' is an integer identifier that identifies /// the graphics node that drew it. A 'hit color' can therefore /// identify the graphics node that corresponds an x-y coordinates. /// private Bitmap idMapRaster; /// /// The renderer's GraphicsWrapper /// object. /// private GdiGraphicsWrapper graphics; /// /// The renderer's back color. /// private Color backColor; /// /// The renderer's SvgWindow object. /// private ISvgWindow window; /// /// /// private float currentDownX; private float currentDownY; private IEventTarget currentTarget; private IEventTarget currentDownTarget; private GdiRenderingHelper _svgRenderer; private SvgRectF invalidRect = SvgRectF.Empty; #endregion #region Constructors and Destructor /// /// Initializes a new instance of the GdiRenderer class. /// public GdiGraphicsRenderer() { counter = 0; _svgRenderer = new GdiRenderingHelper(this); backColor = Color.White; } ~GdiGraphicsRenderer() { this.Dispose(false); } #endregion #region Public Properties /// /// Gets a bitmap image of the a rendered Svg document. /// public Bitmap RasterImage { get { return rasterImage; } } /// /// Gets the image map of the rendered Svg document. This /// is a picture of how the renderer will map the (x,y) positions /// of mouse events to objects. You can display this raster /// to help in debugging of hit testing. /// public Bitmap IdMapRaster { get { return idMapRaster; } } /// /// Gets or sets the Window of the /// renderer. /// /// /// The Window of the renderer. /// public ISvgWindow Window { get { return window; } set { window = value; } } /// /// Gets or sets the back color of the renderer. /// /// /// The back color of the renderer. /// public Color BackColor { get { return backColor; } set { backColor = value; } } /// /// Gets or sets the GraphicsWrapper /// object associated with this renderer. /// /// /// The GraphicsWrapper object /// associated with this renderer. /// public GdiGraphicsWrapper GraphicsWrapper { get { return graphics; } set { graphics = value; } } /// /// Gets or sets the Graphics object /// associated with this renderer. /// /// /// The Graphics object associated /// with this renderer. /// public Graphics Graphics { get { return graphics.Graphics; } set { graphics.Graphics = value; } } #endregion #region Public Methods public void InvalidateRect(SvgRectF rect) { if (invalidRect == SvgRectF.Empty) invalidRect = rect; else invalidRect.Intersect(rect); } /// /// Renders the SvgElement. /// /// /// The SvgElement node to be /// rendered /// /// /// The bitmap on which the rendering was performed. /// public void Render(ISvgElement node) { SvgRectF updatedRect; if (invalidRect != SvgRectF.Empty) updatedRect = new SvgRectF(invalidRect.X, invalidRect.Y, invalidRect.Width, invalidRect.Height); else updatedRect = SvgRectF.Empty; RendererBeforeRender(); if (graphics != null && graphics.Graphics != null) { _svgRenderer.Render(node); } RendererAfterRender(); if (onRender != null) OnRender(updatedRect); } /// /// Renders the SvgDocument. /// /// /// The SvgDocument node to be /// rendered /// /// /// The bitmap on which the rendering was performed. /// public void Render(ISvgDocument node) { SvgRectF updatedRect; if (invalidRect != SvgRectF.Empty) updatedRect = new SvgRectF(invalidRect.X, invalidRect.Y, invalidRect.Width, invalidRect.Height); else updatedRect = SvgRectF.Empty; RendererBeforeRender(); if (graphics != null && graphics.Graphics != null) { _svgRenderer.Render(node); } RendererAfterRender(); if (onRender != null) OnRender(updatedRect); } public void RenderChildren(ISvgElement node) { _svgRenderer.RenderChildren(node); } public void ClearMap() { graphicsNodes.Clear(); } /// /// The invalidated region /// public SvgRectF InvalidRect { get { return invalidRect; } set { invalidRect = value; } } public ISvgRect GetRenderedBounds(ISvgElement element, float margin) { SvgTransformableElement transElement = element as SvgTransformableElement; if (transElement != null) { SvgRectF rect = this.GetElementBounds(transElement, margin); return new SvgRect(rect.X, rect.Y, rect.Width, rect.Height); } return null; } #endregion #region Event handlers private RenderEvent onRender; public RenderEvent OnRender { get { return onRender; } set { onRender = value; } } /// /// Processes mouse events. /// /// /// A string describing the type of mouse event that occured. /// /// /// The MouseEventArgs that contains /// the event data. /// public void OnMouseEvent(string type, MouseEventArgs e) { if (idMapRaster != null) { try { Color pixel = idMapRaster.GetPixel(e.X, e.Y); SvgElement grElement = GetElementFromColor(pixel); if (grElement != null) { IEventTarget target; if (grElement.ElementInstance != null) target = grElement.ElementInstance as IEventTarget; else target = grElement as IEventTarget; if (target != null) { switch (type) { case "mousemove": { if (currentTarget == target) { target.DispatchEvent(new MouseEvent( EventType.MouseMove, true, false, null, // todo: put view here 0, // todo: put detail here e.X, e.Y, e.X, e.Y, false, false, false, false, 0, null, false)); } else { if (currentTarget != null) { currentTarget.DispatchEvent(new MouseEvent( EventType.MouseOut, true, false, null, // todo: put view here 0, // todo: put detail here e.X, e.Y, e.X, e.Y, false, false, false, false, 0, null, false)); } target.DispatchEvent(new MouseEvent( EventType.MouseOver, true, false, null, // todo: put view here 0, // todo: put detail here e.X, e.Y, e.X, e.Y, false, false, false, false, 0, null, false)); } break; } case "mousedown": target.DispatchEvent(new MouseEvent( EventType.MouseDown, true, false, null, // todo: put view here 0, // todo: put detail here e.X, e.Y, e.X, e.Y, false, false, false, false, 0, null, false)); currentDownTarget = target; currentDownX = e.X; currentDownY = e.Y; break; case "mouseup": target.DispatchEvent(new MouseEvent( EventType.MouseUp, true, false, null, // todo: put view here 0, // todo: put detail here e.X, e.Y, e.X, e.Y, false, false, false, false, 0, null, false)); if (/*currentDownTarget == target &&*/ Math.Abs(currentDownX - e.X) < 5 && Math.Abs(currentDownY - e.Y) < 5) { target.DispatchEvent(new MouseEvent( EventType.Click, true, false, null, // todo: put view here 0, // todo: put detail here e.X, e.Y, e.X, e.Y, false, false, false, false, 0, null, false)); } currentDownTarget = null; currentDownX = 0; currentDownY = 0; break; } currentTarget = target; } else { // jr patch if (currentTarget != null && type == "mousemove") { currentTarget.DispatchEvent(new MouseEvent( EventType.MouseOut, true, false, null, // todo: put view here 0, // todo: put detail here e.X, e.Y, e.X, e.Y, false, false, false, false, 0, null, false)); } currentTarget = null; } } else { // jr patch if (currentTarget != null && type == "mousemove") { currentTarget.DispatchEvent(new MouseEvent( EventType.MouseOut, true, false, null, // todo: put view here 0, // todo: put detail here e.X, e.Y, e.X, e.Y, false, false, false, false, 0, null, false)); } currentTarget = null; } } catch { } } } #endregion #region Miscellaneous Methods /// /// Allocate a hit color for the specified graphics node. /// /// /// The GraphicsNode object for which to /// allocate a new hit color. /// /// /// The hit color for the GraphicsNode /// object. /// internal Color GetNextColor(SvgElement element) { // TODO: [newhoggy] It looks like there is a potential memory leak here. // We only ever add to the graphicsNodes map, never remove // from it, so it will grow every time this function is called. // The counter is used to generate IDs in the range [0,2^24-1] // The 24 bits of the counter are interpreted as follows: // [red 7 bits | green 7 bits | blue 7 bits |shuffle term 3 bits] // The shuffle term is used to define how the remaining high // bit is set on each color. The colors are generated in the // range [0,127] (7 bits) instead of [0,255]. Then the shuffle term // is used to adjust them into the range [0,255]. // This algorithm has the feature that consecutive ids generate // visually distinct colors. int id = counter++; // Zero should be the first color. int shuffleTerm = id & 7; int r = 0x7f & (id >> 17); int g = 0x7f & (id >> 10); int b = 0x7f & (id >> 3); switch (shuffleTerm) { case 0: break; case 1: b |= 0x80; break; case 2: g |= 0x80; break; case 3: g |= 0x80; b |= 0x80; break; case 4: r |= 0x80; break; case 5: r |= 0x80; b |= 0x80; break; case 6: r |= 0x80; g |= 0x80; break; case 7: r |= 0x80; g |= 0x80; b |= 0x80; break; } Color color = Color.FromArgb(r, g, b); graphicsNodes.Add(color, element); return color; } internal void RemoveColor(Color color, SvgElement element) { if (!color.IsEmpty) { graphicsNodes[color] = null; graphicsNodes.Remove(color); } } /// /// Gets the GraphicsNode object that /// corresponds to the given hit color. /// /// /// The hit color for which to get the corresponding /// GraphicsNode object. /// /// /// Returns null if a corresponding /// GraphicsNode object cannot be /// found for the given hit color. /// /// /// The GraphicsNode object that /// corresponds to the given hit color /// private SvgElement GetElementFromColor(Color color) { if (color.A == 0) { return null; } else { if (graphicsNodes.ContainsKey(color)) { return graphicsNodes[color]; } return null; } } /// /// TODO: This method is not used. /// /// /// /// /// private static int ColorToId(Color color) { int r = color.R; int g = color.G; int b = color.B; int shuffleTerm = 0; if (0 != (r & 0x80)) { shuffleTerm |= 4; r &= 0x7f; } if (0 != (g & 0x80)) { shuffleTerm |= 2; g &= 0x7f; } if (0 != (b & 0x80)) { shuffleTerm |= 1; b &= 0x7f; } return (r << 17) + (g << 10) + (b << 3) + shuffleTerm; } private SvgRectF GetElementBounds(SvgTransformableElement element, float margin) { SvgRenderingHint hint = element.RenderingHint; if (hint == SvgRenderingHint.Shape || hint == SvgRenderingHint.Text) { GraphicsPath gp = GdiRendering.CreatePath(element); ISvgMatrix svgMatrix = element.GetScreenCTM(); Matrix matrix = new Matrix((float)svgMatrix.A, (float)svgMatrix.B, (float)svgMatrix.C, (float)svgMatrix.D, (float)svgMatrix.E, (float)svgMatrix.F); SvgRectF bounds = SvgConverter.ToRect(gp.GetBounds(matrix)); bounds = SvgRectF.Inflate(bounds, margin, margin); return bounds; } SvgUseElement useElement = element as SvgUseElement; if (useElement != null) { SvgTransformableElement refEl = useElement.ReferencedElement as SvgTransformableElement; if (refEl == null) return SvgRectF.Empty; XmlElement refElParent = (XmlElement)refEl.ParentNode; element.OwnerDocument.Static = true; useElement.CopyToReferencedElement(refEl); element.AppendChild(refEl); SvgRectF bbox = this.GetElementBounds(refEl, margin); element.RemoveChild(refEl); useElement.RestoreReferencedElement(refEl); refElParent.AppendChild(refEl); element.OwnerDocument.Static = false; return bbox; } SvgRectF union = SvgRectF.Empty; SvgTransformableElement transformChild; foreach (XmlNode childNode in element.ChildNodes) { if (childNode is SvgDefsElement) continue; if (childNode is ISvgTransformable) { transformChild = (SvgTransformableElement)childNode; SvgRectF bbox = this.GetElementBounds(transformChild, margin); if (bbox != SvgRectF.Empty) { if (union == SvgRectF.Empty) union = bbox; else union = SvgRectF.Union(union, bbox); } } } return union; } #endregion #region Private Methods /// /// BeforeRender - Make sure we have a Graphics object to render to. /// If we don't have one, then create one to match the SvgWindow's /// physical dimensions. /// private void RendererBeforeRender() { // Testing for null here allows "advanced" developers to create their own Graphics object for rendering if (graphics == null) { // Get the current SVGWindow's width and height int innerWidth = (int)window.InnerWidth; int innerHeight = (int)window.InnerHeight; // Make sure we have an actual area to render to if (innerWidth > 0 && innerHeight > 0) { // See if we already have a rasterImage that matches the current SVGWindow dimensions if (rasterImage == null || rasterImage.Width != innerWidth || rasterImage.Height != innerHeight) { // Nope, so create one if (rasterImage != null) { rasterImage.Dispose(); rasterImage = null; } rasterImage = new Bitmap(innerWidth, innerHeight); } // Maybe we are only repainting an invalidated section if (invalidRect != SvgRectF.Empty) { // TODO: Worry about pan... if (invalidRect.X < 0) invalidRect.X = 0; if (invalidRect.Y < 0) invalidRect.Y = 0; if (invalidRect.Right > innerWidth) invalidRect.Width = innerWidth - invalidRect.X; if (invalidRect.Bottom > innerHeight) invalidRect.Height = innerHeight - invalidRect.Y; if (invalidatedRasterImage == null || invalidatedRasterImage.Width < invalidRect.Right || invalidatedRasterImage.Height < invalidRect.Bottom) { // Nope, so create one if (invalidatedRasterImage != null) { invalidatedRasterImage.Dispose(); invalidatedRasterImage = null; } invalidatedRasterImage = new Bitmap((int)invalidRect.Right, (int)invalidRect.Bottom); } // Make a GraphicsWrapper object from the regionRasterImage and clear it to the background color graphics = GdiGraphicsWrapper.FromImage(invalidatedRasterImage, false); graphics.Clear(backColor); } else { // Make a GraphicsWrapper object from the rasterImage and clear it to the background color graphics = GdiGraphicsWrapper.FromImage(rasterImage, false); graphics.Clear(backColor); } } } } /// /// AfterRender - Dispose of Graphics object created for rendering. /// private void RendererAfterRender() { if (graphics != null) { // Check if we only invalidated a rect if (invalidRect != SvgRectF.Empty) { // We actually drew everything on invalidatedRasterImage and now we // need to copy that to rasterImage Graphics tempGraphics = Graphics.FromImage(rasterImage); tempGraphics.DrawImage(invalidatedRasterImage, invalidRect.X, invalidRect.Y, GdiConverter.ToRectangle(invalidRect), GraphicsUnit.Pixel); tempGraphics.Dispose(); tempGraphics = null; // If we currently have an idMapRaster here, then we need to create // a temporary graphics object to draw the invalidated portion from // our main graphics window onto it. if (idMapRaster != null) { tempGraphics = Graphics.FromImage(idMapRaster); tempGraphics.DrawImage(graphics.IdMapRaster, invalidRect.X, invalidRect.Y, GdiConverter.ToRectangle(invalidRect), GraphicsUnit.Pixel); tempGraphics.Dispose(); tempGraphics = null; } else { idMapRaster = graphics.IdMapRaster; } // We have updated the invalid region invalidRect = SvgRectF.Empty; } else { if (idMapRaster != null && idMapRaster != graphics.IdMapRaster) idMapRaster.Dispose(); idMapRaster = graphics.IdMapRaster; } graphics.Dispose(); graphics = null; } } #endregion #region IDisposable Members public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (idMapRaster != null) idMapRaster.Dispose(); if (invalidatedRasterImage != null) invalidatedRasterImage.Dispose(); if (rasterImage != null) rasterImage.Dispose(); if (graphics != null) graphics.Dispose(); } #endregion } }