SvgVisualElement.cs 18 KB
Newer Older
davescriven's avatar
davescriven committed
1
2
3
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
Eric Domke's avatar
Eric Domke committed
4
using System.Diagnostics;
Eric Domke's avatar
Eric Domke committed
5
using System.Linq;
davescriven's avatar
davescriven committed
6
7
8
9
10
11

namespace Svg
{
    /// <summary>
    /// The class that all SVG elements should derive from when they are to be rendered.
    /// </summary>
James Welle's avatar
James Welle committed
12
    public abstract partial class SvgVisualElement : SvgElement, ISvgBoundable, ISvgStylable, ISvgClipable
davescriven's avatar
davescriven committed
13
    {
14
        private bool? _requiresSmoothRendering;
15
        private Region _previousClip;
davescriven's avatar
davescriven committed
16
17
18
19

        /// <summary>
        /// Gets the <see cref="GraphicsPath"/> for this element.
        /// </summary>
Eric Domke's avatar
Eric Domke committed
20
        public abstract GraphicsPath Path(ISvgRenderer renderer);
James Welle's avatar
James Welle committed
21

Tebjan Halm's avatar
Tebjan Halm committed
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
        PointF ISvgBoundable.Location
        {
            get
            {
                return Bounds.Location;
            }
        }

        SizeF ISvgBoundable.Size
        {
            get
            {
                return Bounds.Size;
            }
        }

davescriven's avatar
davescriven committed
38
39
40
        /// <summary>
        /// Gets the bounds of the element.
        /// </summary>
Tebjan Halm's avatar
Tebjan Halm committed
41
42
        /// <value>The bounds.</value>
        public abstract RectangleF Bounds { get; }
davescriven's avatar
davescriven committed
43

Eric Domke's avatar
Eric Domke committed
44
45
46
47
48
49
50
51
52
53
        /// <summary>
        /// Gets the associated <see cref="SvgClipPath"/> if one has been specified.
        /// </summary>
        [SvgAttribute("clip")]
        public virtual string Clip
        {
            get { return this.Attributes.GetInheritedAttribute<string>("clip"); }
            set { this.Attributes["clip"] = value; }
        }

54
55
56
57
58
59
        /// <summary>
        /// Gets the associated <see cref="SvgClipPath"/> if one has been specified.
        /// </summary>
        [SvgAttribute("clip-path")]
        public virtual Uri ClipPath
        {
60
            get { return this.Attributes.GetAttribute<Uri>("clip-path"); }
61
62
63
            set { this.Attributes["clip-path"] = value; }
        }

64
        /// <summary>
65
        /// Gets or sets the algorithm which is to be used to determine the clipping region.
66
67
68
69
70
71
72
73
        /// </summary>
        [SvgAttribute("clip-rule")]
        public SvgClipRule ClipRule
        {
            get { return this.Attributes.GetAttribute<SvgClipRule>("clip-rule", SvgClipRule.NonZero); }
            set { this.Attributes["clip-rule"] = value; }
        }

James Welle's avatar
James Welle committed
74
75
76
77
78
79
        /// <summary>
        /// Gets the associated <see cref="SvgClipPath"/> if one has been specified.
        /// </summary>
        [SvgAttribute("filter")]
        public virtual Uri Filter
        {
Eric Domke's avatar
Eric Domke committed
80
            get { return this.Attributes.GetInheritedAttribute<Uri>("filter"); }
James Welle's avatar
James Welle committed
81
82
            set { this.Attributes["filter"] = value; }
        }
83

davescriven's avatar
davescriven committed
84
85
86
87
88
        /// <summary>
        /// Gets or sets a value to determine if anti-aliasing should occur when the element is being rendered.
        /// </summary>
        protected virtual bool RequiresSmoothRendering
        {
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
            get
            {
                if (_requiresSmoothRendering == null)
                    _requiresSmoothRendering = ConvertShapeRendering2AntiAlias(ShapeRendering);

                return _requiresSmoothRendering.Value;
            }
        }

        private bool ConvertShapeRendering2AntiAlias(SvgShapeRendering shapeRendering)
        {
            switch (shapeRendering)
            {
                case SvgShapeRendering.OptimizeSpeed:
                case SvgShapeRendering.CrispEdges:
                case SvgShapeRendering.GeometricPrecision:
                    return false;
                default:
                    // SvgShapeRendering.Auto
                    // SvgShapeRendering.Inherit
                    return true;
            }
davescriven's avatar
davescriven committed
111
112
113
        }

        /// <summary>
114
        /// Initializes a new instance of the <see cref="SvgVisualElement"/> class.
davescriven's avatar
davescriven committed
115
        /// </summary>
116
        public SvgVisualElement()
davescriven's avatar
davescriven committed
117
        {
Eric Domke's avatar
Eric Domke committed
118
            this.IsPathDirty = true;
davescriven's avatar
davescriven committed
119
120
        }

Eric Domke's avatar
Eric Domke committed
121
122
        protected virtual bool Renderable { get { return true; } }

davescriven's avatar
davescriven committed
123
124
125
        /// <summary>
        /// Renders the <see cref="SvgElement"/> and contents to the specified <see cref="Graphics"/> object.
        /// </summary>
Eric Domke's avatar
Eric Domke committed
126
127
        /// <param name="renderer">The <see cref="ISvgRenderer"/> object to render to.</param>
        protected override void Render(ISvgRenderer renderer)
davescriven's avatar
davescriven committed
128
        {
Eric Domke's avatar
Eric Domke committed
129
130
131
            this.Render(renderer, true);
        }

Eric Domke's avatar
Eric Domke committed
132
        private void Render(ISvgRenderer renderer, bool renderFilter)
Eric Domke's avatar
Eric Domke committed
133
134
135
        {
            if (this.Visible && this.Displayable && this.PushTransforms(renderer) &&
                (!Renderable || this.Path(renderer) != null))
davescriven's avatar
davescriven committed
136
            {
Eric Domke's avatar
Eric Domke committed
137
                bool renderNormal = true;
davescriven's avatar
davescriven committed
138

Eric Domke's avatar
Eric Domke committed
139
                if (renderFilter && this.Filter != null)
davescriven's avatar
davescriven committed
140
                {
Eric Domke's avatar
Eric Domke committed
141
142
143
144
145
146
                    var filterPath = this.Filter;
                    if (filterPath.ToString().StartsWith("url("))
                    {
                        filterPath = new Uri(filterPath.ToString().Substring(4, filterPath.ToString().Length - 5), UriKind.RelativeOrAbsolute);
                    }
                    var filter = this.OwnerDocument.IdManager.GetElementById(filterPath) as FilterEffects.SvgFilter;
Eric Domke's avatar
Eric Domke committed
147
148
149
                    if (filter != null)
                    {
                        this.PopTransforms(renderer);
Eric Domke's avatar
Eric Domke committed
150
151
152
153
154
                        try
                        {
                            filter.ApplyFilter(this, renderer, (r) => this.Render(r, false));
                        }
                        catch (Exception ex) { Debug.Print(ex.ToString()); }
Eric Domke's avatar
Eric Domke committed
155
156
                        renderNormal = false;
                    }
davescriven's avatar
davescriven committed
157
158
                }

159

Eric Domke's avatar
Eric Domke committed
160
                if (renderNormal)
davescriven's avatar
davescriven committed
161
                {
Eric Domke's avatar
Eric Domke committed
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
                    this.SetClip(renderer);

                    if (Renderable)
                    {
                        // If this element needs smoothing enabled turn anti-aliasing on
                        if (this.RequiresSmoothRendering)
                        {
                            renderer.SmoothingMode = SmoothingMode.AntiAlias;
                        }

                        this.RenderFill(renderer);
                        this.RenderStroke(renderer);

                        // Reset the smoothing mode
                        if (this.RequiresSmoothRendering && renderer.SmoothingMode == SmoothingMode.AntiAlias)
                        {
                            renderer.SmoothingMode = SmoothingMode.Default;
                        }
                    }
                    else
                    {
                        base.RenderChildren(renderer);
                    }

                    this.ResetClip(renderer);
                    this.PopTransforms(renderer);
188
189
190
191
192
193
                }

            }
        }

        /// <summary>
Eric Domke's avatar
Eric Domke committed
194
        /// Renders the fill of the <see cref="SvgVisualElement"/> to the specified <see cref="ISvgRenderer"/>
195
        /// </summary>
Eric Domke's avatar
Eric Domke committed
196
197
        /// <param name="renderer">The <see cref="ISvgRenderer"/> object to render to.</param>
        protected internal virtual void RenderFill(ISvgRenderer renderer)
198
199
200
        {
            if (this.Fill != null)
            {
Eric Domke's avatar
Eric Domke committed
201
                using (var brush = this.Fill.GetBrush(this, renderer, Math.Min(Math.Max(this.FillOpacity * this.Opacity, 0), 1)))
202
203
                {
                    if (brush != null)
davescriven's avatar
davescriven committed
204
                    {
205
206
                        this.Path(renderer).FillMode = this.FillRule == SvgFillRule.NonZero ? FillMode.Winding : FillMode.Alternate;
                        renderer.FillPath(brush, this.Path(renderer));
davescriven's avatar
davescriven committed
207
208
                    }
                }
209
210
            }
        }
davescriven's avatar
davescriven committed
211

212
        /// <summary>
Eric Domke's avatar
Eric Domke committed
213
        /// Renders the stroke of the <see cref="SvgVisualElement"/> to the specified <see cref="ISvgRenderer"/>
214
        /// </summary>
Eric Domke's avatar
Eric Domke committed
215
        /// <param name="renderer">The <see cref="ISvgRenderer"/> object to render to.</param>
Eric Domke's avatar
Eric Domke committed
216
        protected internal virtual bool RenderStroke(ISvgRenderer renderer)
217
        {
218
            if (this.Stroke != null && this.Stroke != SvgColourServer.None && this.StrokeWidth > 0)
219
            {
220
                float strokeWidth = this.StrokeWidth.ToDeviceValue(renderer, UnitRenderingType.Other, this);
Eric Domke's avatar
Eric Domke committed
221
                using (var brush = this.Stroke.GetBrush(this, renderer, Math.Min(Math.Max(this.StrokeOpacity * this.Opacity, 0), 1), true))
davescriven's avatar
davescriven committed
222
                {
Eric Domke's avatar
Eric Domke committed
223
                    if (brush != null)
davescriven's avatar
davescriven committed
224
                    {
Eric Domke's avatar
Eric Domke committed
225
226
227
228
                        var path = this.Path(renderer);
                        var bounds = path.GetBounds();
                        if (path.PointCount < 1) return false;
                        if (bounds.Width <= 0 && bounds.Height <= 0)
Eric Domke's avatar
Eric Domke committed
229
                        {
Eric Domke's avatar
Eric Domke committed
230
                            switch (this.StrokeLineCap)
Eric Domke's avatar
Eric Domke committed
231
                            {
Eric Domke's avatar
Eric Domke committed
232
233
234
235
236
237
238
239
240
241
242
243
244
245
                                case SvgStrokeLineCap.Round:
                                    using (var capPath = new GraphicsPath())
                                    {
                                        capPath.AddEllipse(path.PathPoints[0].X - strokeWidth / 2, path.PathPoints[0].Y - strokeWidth / 2, strokeWidth, strokeWidth);
                                        renderer.FillPath(brush, capPath);
                                    }
                                    break;
                                case SvgStrokeLineCap.Square:
                                    using (var capPath = new GraphicsPath())
                                    {
                                        capPath.AddRectangle(new RectangleF(path.PathPoints[0].X - strokeWidth / 2, path.PathPoints[0].Y - strokeWidth / 2, strokeWidth, strokeWidth));
                                        renderer.FillPath(brush, capPath);
                                    }
                                    break;
Eric Domke's avatar
Eric Domke committed
246
                            }
Eric Domke's avatar
Eric Domke committed
247
248
249
250
251
252
253
                        }
                        else
                        {
                            using (var pen = new Pen(brush, strokeWidth))
                            {
                                if (this.StrokeDashArray != null && this.StrokeDashArray.Count > 0)
                                {
254
255
256
257
258
                                    if (this.StrokeDashArray.Count % 2 != 0)
                                    {
                                        // handle odd dash arrays by repeating them once
                                        this.StrokeDashArray.AddRange(this.StrokeDashArray);
                                    }
259

Eric Domke's avatar
Eric Domke committed
260
261
262
                                    /* divide by stroke width - GDI behaviour that I don't quite understand yet.*/
                                    pen.DashPattern = this.StrokeDashArray.ConvertAll(u => ((u.ToDeviceValue(renderer, UnitRenderingType.Other, this) <= 0) ? 1 : u.ToDeviceValue(renderer, UnitRenderingType.Other, this)) /
                                        ((strokeWidth <= 0) ? 1 : strokeWidth)).ToArray();
263

264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
                                    if (this.StrokeLineCap == SvgStrokeLineCap.Round)
                                    {
                                        // to handle round caps, we have to adapt the dash pattern
                                        // by increasing the dash length by the stroke width - GDI draws the rounded 
                                        // edge inside the dash line, SVG draws it outside the line  
                                        var pattern = new float[pen.DashPattern.Length];
                                        int offset = 1; // the values are already normalized to dash width
                                        for ( int i = 0; i < pen.DashPattern.Length; i++)                               
                                        {
                                            pattern[i] = pen.DashPattern[i] + offset;
                                            offset *= -1; // increase dash length, decrease spaces
                                        }
                                        pen.DashPattern = pattern;
                                        pen.DashCap = DashCap.Round;
                                    }

280
281
282
283
284
                                    if (this.StrokeDashOffset != null && this.StrokeDashOffset.Value != 0)
                                    {
                                        pen.DashOffset = ((this.StrokeDashOffset.ToDeviceValue(renderer, UnitRenderingType.Other, this) <= 0) ? 1 : this.StrokeDashOffset.ToDeviceValue(renderer, UnitRenderingType.Other, this)) /
                                            ((strokeWidth <= 0) ? 1 : strokeWidth);
                                    }
Eric Domke's avatar
Eric Domke committed
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
                                }
                                switch (this.StrokeLineJoin)
                                {
                                    case SvgStrokeLineJoin.Bevel:
                                        pen.LineJoin = LineJoin.Bevel;
                                        break;
                                    case SvgStrokeLineJoin.Round:
                                        pen.LineJoin = LineJoin.Round;
                                        break;
                                    default:
                                        pen.LineJoin = LineJoin.Miter;
                                        break;
                                }
                                pen.MiterLimit = this.StrokeMiterLimit;
                                switch (this.StrokeLineCap)
                                {
                                    case SvgStrokeLineCap.Round:
                                        pen.StartCap = LineCap.Round;
                                        pen.EndCap = LineCap.Round;
                                        break;
                                    case SvgStrokeLineCap.Square:
                                        pen.StartCap = LineCap.Square;
                                        pen.EndCap = LineCap.Square;
                                        break;
                                }

                                renderer.DrawPath(pen, path);
312

Eric Domke's avatar
Eric Domke committed
313
314
                                return true;
                            }
Eric Domke's avatar
Eric Domke committed
315
316
                        }
                    }
davescriven's avatar
davescriven committed
317
318
                }
            }
Eric Domke's avatar
Eric Domke committed
319
320

            return false;
davescriven's avatar
davescriven committed
321
        }
322

323
        /// <summary>
Eric Domke's avatar
Eric Domke committed
324
        /// Sets the clipping region of the specified <see cref="ISvgRenderer"/>.
325
        /// </summary>
Eric Domke's avatar
Eric Domke committed
326
327
        /// <param name="renderer">The <see cref="ISvgRenderer"/> to have its clipping region set.</param>
        protected internal virtual void SetClip(ISvgRenderer renderer)
328
        {
Eric Domke's avatar
Eric Domke committed
329
            if (this.ClipPath != null || !string.IsNullOrEmpty(this.Clip))
330
            {
Eric Domke's avatar
Eric Domke committed
331
                this._previousClip = renderer.GetClip();
332

Eric Domke's avatar
Eric Domke committed
333
334
335
336
337
338
339
340
                if (this.ClipPath != null)
                {
                    SvgClipPath clipPath = this.OwnerDocument.GetElementById<SvgClipPath>(this.ClipPath.ToString());
                    if (clipPath != null) renderer.SetClip(clipPath.GetClipRegion(this), CombineMode.Intersect);
                }

                var clip = this.Clip;
                if (!string.IsNullOrEmpty(clip) && clip.StartsWith("rect("))
341
                {
Eric Domke's avatar
Eric Domke committed
342
343
344
345
346
347
348
349
                    clip = clip.Trim();
                    var offsets = (from o in clip.Substring(5, clip.Length - 6).Split(',')
                                   select float.Parse(o.Trim())).ToList();
                    var bounds = this.Bounds;
                    var clipRect = new RectangleF(bounds.Left + offsets[3], bounds.Top + offsets[0],
                                                  bounds.Width - (offsets[3] + offsets[1]),
                                                  bounds.Height - (offsets[2] + offsets[0]));
                    renderer.SetClip(new Region(clipRect), CombineMode.Intersect);
350
                }
351
352
353
            }
        }

354
        /// <summary>
Eric Domke's avatar
Eric Domke committed
355
        /// Resets the clipping region of the specified <see cref="ISvgRenderer"/> back to where it was before the <see cref="SetClip"/> method was called.
356
        /// </summary>
Eric Domke's avatar
Eric Domke committed
357
358
        /// <param name="renderer">The <see cref="ISvgRenderer"/> to have its clipping region reset.</param>
        protected internal virtual void ResetClip(ISvgRenderer renderer)
359
        {
360
            if (this._previousClip != null)
361
            {
Eric Domke's avatar
Eric Domke committed
362
                renderer.SetClip(this._previousClip);
363
364
365
366
                this._previousClip = null;
            }
        }

367
        /// <summary>
Eric Domke's avatar
Eric Domke committed
368
        /// Sets the clipping region of the specified <see cref="ISvgRenderer"/>.
369
        /// </summary>
Eric Domke's avatar
Eric Domke committed
370
371
        /// <param name="renderer">The <see cref="ISvgRenderer"/> to have its clipping region set.</param>
        void ISvgClipable.SetClip(ISvgRenderer renderer)
372
373
374
375
        {
            this.SetClip(renderer);
        }

376
        /// <summary>
Eric Domke's avatar
Eric Domke committed
377
        /// Resets the clipping region of the specified <see cref="ISvgRenderer"/> back to where it was before the <see cref="SetClip"/> method was called.
378
        /// </summary>
Eric Domke's avatar
Eric Domke committed
379
380
        /// <param name="renderer">The <see cref="ISvgRenderer"/> to have its clipping region reset.</param>
        void ISvgClipable.ResetClip(ISvgRenderer renderer)
381
382
383
        {
            this.ResetClip(renderer);
        }
384

James Welle's avatar
James Welle committed
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
        public override SvgElement DeepCopy<T>()
        {
            var newObj = base.DeepCopy<T>() as SvgVisualElement;
            newObj.ClipPath = this.ClipPath;
            newObj.ClipRule = this.ClipRule;
            newObj.Filter = this.Filter;

            newObj.Visible = this.Visible;
            if (this.Fill != null)
                newObj.Fill = this.Fill;
            if (this.Stroke != null)
                newObj.Stroke = this.Stroke;
            newObj.FillRule = this.FillRule;
            newObj.FillOpacity = this.FillOpacity;
            newObj.StrokeWidth = this.StrokeWidth;
            newObj.StrokeLineCap = this.StrokeLineCap;
            newObj.StrokeLineJoin = this.StrokeLineJoin;
            newObj.StrokeMiterLimit = this.StrokeMiterLimit;
            newObj.StrokeDashArray = this.StrokeDashArray;
            newObj.StrokeDashOffset = this.StrokeDashOffset;
            newObj.StrokeOpacity = this.StrokeOpacity;
            newObj.Opacity = this.Opacity;

            return newObj;
        }
410

davescriven's avatar
davescriven committed
411
    }
412
}