SvgDocument.cs 19.5 KB
Newer Older
davescriven's avatar
davescriven committed
1
2
3
4
5
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
6
7
using System.Drawing.Drawing2D;
using System.Drawing.Text;
davescriven's avatar
davescriven committed
8
9
10
using System.IO;
using System.Text;
using System.Xml;
11
using System.Linq;
Eric Domke's avatar
Eric Domke committed
12
13
using ExCSS;
using Svg.Css;
davescriven's avatar
davescriven committed
14
15
16
17

namespace Svg
{
    /// <summary>
18
    /// The class used to create and load SVG documents.
davescriven's avatar
davescriven committed
19
20
21
    /// </summary>
    public class SvgDocument : SvgFragment, ITypeDescriptorContext
    {
22
        public static readonly int PointsPerInch = 96;
23
        private SvgElementIdManager _idManager;
24

Eric Domke's avatar
Eric Domke committed
25
26
27
28
29
30
31
32
33
34
35
36
        private Dictionary<string, IEnumerable<SvgFontFace>> _fontDefns = null;
        internal Dictionary<string, IEnumerable<SvgFontFace>> FontDefns()
        {
            if (_fontDefns == null)
            {
                _fontDefns = (from f in Descendants().OfType<SvgFontFace>()
                              group f by f.FontFamily into family
                              select family).ToDictionary(f => f.Key, f => (IEnumerable<SvgFontFace>)f);
            }
            return _fontDefns;
        }

37
38
39
        /// <summary>
        /// Initializes a new instance of the <see cref="SvgDocument"/> class.
        /// </summary>
40
        public SvgDocument()
davescriven's avatar
davescriven committed
41
        {
James Welle's avatar
James Welle committed
42
            Ppi = PointsPerInch;
davescriven's avatar
davescriven committed
43
44
        }

45
46
        public Uri BaseUri { get; set; }

davescriven's avatar
davescriven committed
47
48
49
50
51
52
53
        /// <summary>
        /// Gets an <see cref="SvgElementIdManager"/> for this document.
        /// </summary>
        protected internal virtual SvgElementIdManager IdManager
        {
            get
            {
54
                if (_idManager == null)
55
                {
56
                    _idManager = new SvgElementIdManager(this);
57
                }
davescriven's avatar
davescriven committed
58

59
                return _idManager;
davescriven's avatar
davescriven committed
60
61
            }
        }
Eric Domke's avatar
Eric Domke committed
62

63
64
65
66
67
68
69
70
        /// <summary>
        /// Overwrites the current IdManager with a custom implementation. 
        /// Be careful with this: If elements have been inserted into the document before,
        /// you have to take care that the new IdManager also knows of them.
        /// </summary>
        /// <param name="manager"></param>
        public void OverwriteIdManager(SvgElementIdManager manager)
        {
James Welle's avatar
James Welle committed
71
            _idManager = manager;
72
        }
davescriven's avatar
davescriven committed
73

74
75
76
        /// <summary>
        /// Gets or sets the Pixels Per Inch of the rendered image.
        /// </summary>
77
        public int Ppi { get; set; }
sschurig's avatar
sschurig committed
78
79
80
81
82
        
        /// <summary>
        /// Gets or sets an external Cascading Style Sheet (CSS)
        /// </summary>
        public string ExternalCSSHref { get; set; }        
83
84
85
86

        #region ITypeDescriptorContext Members

        IContainer ITypeDescriptorContext.Container
davescriven's avatar
davescriven committed
87
        {
88
            get { throw new NotImplementedException(); }
davescriven's avatar
davescriven committed
89
90
        }

91
        object ITypeDescriptorContext.Instance
davescriven's avatar
davescriven committed
92
        {
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
            get { return this; }
        }

        void ITypeDescriptorContext.OnComponentChanged()
        {
            throw new NotImplementedException();
        }

        bool ITypeDescriptorContext.OnComponentChanging()
        {
            throw new NotImplementedException();
        }

        PropertyDescriptor ITypeDescriptorContext.PropertyDescriptor
        {
            get { throw new NotImplementedException(); }
        }

        object IServiceProvider.GetService(Type serviceType)
        {
            throw new NotImplementedException();
        }

        #endregion

        /// <summary>
        /// Retrieves the <see cref="SvgElement"/> with the specified ID.
        /// </summary>
        /// <param name="id">A <see cref="string"/> containing the ID of the element to find.</param>
        /// <returns>An <see cref="SvgElement"/> of one exists with the specified ID; otherwise false.</returns>
        public virtual SvgElement GetElementById(string id)
        {
            return IdManager.GetElementById(id);
davescriven's avatar
davescriven committed
126
127
        }

128
129
130
131
132
133
134
135
136
        /// <summary>
        /// Retrieves the <see cref="SvgElement"/> with the specified ID.
        /// </summary>
        /// <param name="id">A <see cref="string"/> containing the ID of the element to find.</param>
        /// <returns>An <see cref="SvgElement"/> of one exists with the specified ID; otherwise false.</returns>
        public virtual TSvgElement GetElementById<TSvgElement>(string id) where TSvgElement : SvgElement
        {
            return (this.GetElementById(id) as TSvgElement);
        }
Eric Domke's avatar
Eric Domke committed
137
138

        /// <summary>
139
140
141
142
143
144
145
146
147
        /// Opens the document at the specified path and loads the SVG contents.
        /// </summary>
        /// <param name="path">A <see cref="string"/> containing the path of the file to open.</param>
        /// <returns>An <see cref="SvgDocument"/> with the contents loaded.</returns>
        /// <exception cref="FileNotFoundException">The document at the specified <paramref name="path"/> cannot be found.</exception>
        public static SvgDocument Open(string path)
        {
            return Open<SvgDocument>(path, null);
        }
148

davescriven's avatar
davescriven committed
149
        /// <summary>
150
        /// Opens the document at the specified path and loads the SVG contents.
davescriven's avatar
davescriven committed
151
152
153
154
        /// </summary>
        /// <param name="path">A <see cref="string"/> containing the path of the file to open.</param>
        /// <returns>An <see cref="SvgDocument"/> with the contents loaded.</returns>
        /// <exception cref="FileNotFoundException">The document at the specified <paramref name="path"/> cannot be found.</exception>
155
        public static T Open<T>(string path) where T : SvgDocument, new()
156
        {
157
            return Open<T>(path, null);
158
159
        }

160
        /// <summary>
161
        /// Opens the document at the specified path and loads the SVG contents.
162
163
164
165
        /// </summary>
        /// <param name="path">A <see cref="string"/> containing the path of the file to open.</param>
        /// <param name="entities">A dictionary of custom entity definitions to be used when resolving XML entities within the document.</param>
        /// <returns>An <see cref="SvgDocument"/> with the contents loaded.</returns>
166
        /// <exception cref="FileNotFoundException">The document at the specified <paramref name="path"/> cannot be found.</exception>
167
        public static T Open<T>(string path, Dictionary<string, string> entities) where T : SvgDocument, new()
davescriven's avatar
davescriven committed
168
        {
169
170
171
172
173
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentNullException("path");
            }

davescriven's avatar
davescriven committed
174
            if (!File.Exists(path))
175
            {
davescriven's avatar
davescriven committed
176
                throw new FileNotFoundException("The specified document cannot be found.", path);
177
            }
davescriven's avatar
davescriven committed
178

Eric Domke's avatar
Eric Domke committed
179
180
181
182
183
184
            using (var stream = File.OpenRead(path))
            {
                var doc = Open<T>(stream, entities);
                doc.BaseUri = new Uri(System.IO.Path.GetFullPath(path));
                return doc;
            }
davescriven's avatar
davescriven committed
185
186
        }

187
188
189
190
        /// <summary>
        /// Attempts to open an SVG document from the specified <see cref="Stream"/>.
        /// </summary>
        /// <param name="stream">The <see cref="Stream"/> containing the SVG document to open.</param>
191
        public static T Open<T>(Stream stream) where T : SvgDocument, new()
davescriven's avatar
davescriven committed
192
        {
193
            return Open<T>(stream, null);
davescriven's avatar
davescriven committed
194
195
        }

Eric Domke's avatar
Eric Domke committed
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216

        /// <summary>
        /// Attempts to create an SVG document from the specified string data.
        /// </summary>
        /// <param name="svg">The SVG data.</param>
        public static T FromSvg<T>(string svg) where T : SvgDocument, new()
        {
            if (string.IsNullOrEmpty(svg))
            {
                throw new ArgumentNullException("svg");
            }

            using (var strReader = new System.IO.StringReader(svg))
            {
                var reader = new SvgTextReader(strReader, null);
                reader.XmlResolver = new SvgDtdResolver();
                reader.WhitespaceHandling = WhitespaceHandling.None;
                return Open<T>(reader);
            }
        }

217
        /// <summary>
218
        /// Opens an SVG document from the specified <see cref="Stream"/> and adds the specified entities.
219
220
221
        /// </summary>
        /// <param name="stream">The <see cref="Stream"/> containing the SVG document to open.</param>
        /// <param name="entities">Custom entity definitions.</param>
222
        /// <exception cref="ArgumentNullException">The <paramref name="stream"/> parameter cannot be <c>null</c>.</exception>
223
        public static T Open<T>(Stream stream, Dictionary<string, string> entities) where T : SvgDocument, new()
davescriven's avatar
davescriven committed
224
        {
225
226
227
228
229
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

Eric Domke's avatar
Eric Domke committed
230
231
232
233
234
235
            // Don't close the stream via a dispose: that is the client's job.
            var reader = new SvgTextReader(stream, entities);
            reader.XmlResolver = new SvgDtdResolver();
            reader.WhitespaceHandling = WhitespaceHandling.None;
            return Open<T>(reader);
        }
davescriven's avatar
davescriven committed
236

Eric Domke's avatar
Eric Domke committed
237
238
239
240
241
242
243
244
245
246
247
        private static T Open<T>(XmlReader reader) where T : SvgDocument, new()
        {
            var elementStack = new Stack<SvgElement>();
            bool elementEmpty;
            SvgElement element = null;
            SvgElement parent;
            T svgDocument = null;
            
            var styles = new List<ISvgNode>();

            while (reader.Read())
davescriven's avatar
davescriven committed
248
            {
Eric Domke's avatar
Eric Domke committed
249
                try
davescriven's avatar
davescriven committed
250
                {
Eric Domke's avatar
Eric Domke committed
251
                    switch (reader.NodeType)
davescriven's avatar
davescriven committed
252
                    {
Eric Domke's avatar
Eric Domke committed
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
                        case XmlNodeType.Element:
                            // Does this element have a value or children
                            // (Must do this check here before we progress to another node)
                            elementEmpty = reader.IsEmptyElement;
                            // Create element
                            if (elementStack.Count > 0)
                            {
                                element = SvgElementFactory.CreateElement(reader, svgDocument);
                            }
                            else
                            {
                                svgDocument = SvgElementFactory.CreateDocument<T>(reader);
                                element = svgDocument;
                            }

                            // Add to the parents children
                            if (elementStack.Count > 0)
                            {
                                parent = elementStack.Peek();
                                if (parent != null && element != null)
davescriven's avatar
davescriven committed
273
                                {
Eric Domke's avatar
Eric Domke committed
274
275
                                    parent.Children.Add(element);
                                    parent.Nodes.Add(element);
davescriven's avatar
davescriven committed
276
                                }
Eric Domke's avatar
Eric Domke committed
277
278
279
280
281
282
283
284
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
                            }

                            // Push element into stack
                            elementStack.Push(element);

                            // Need to process if the element is empty
                            if (elementEmpty)
                            {
                                goto case XmlNodeType.EndElement;
                            }

                            break;
                        case XmlNodeType.EndElement:

                            // Pop the element out of the stack
                            element = elementStack.Pop();

                            if (element.Nodes.OfType<SvgContentNode>().Any())
                            {
                                element.Content = (from e in element.Nodes select e.Content).Aggregate((p, c) => p + c);
                            }
                            else
                            {
                                element.Nodes.Clear(); // No sense wasting the space where it isn't needed
                            }

                            var unknown = element as SvgUnknownElement;
                            if (unknown != null && unknown.ElementName == "style")
                            {
                                styles.Add(unknown);
                            }
                            break;
                        case XmlNodeType.CDATA:
                        case XmlNodeType.Text:
                            element = elementStack.Peek();
                            element.Nodes.Add(new SvgContentNode() { Content = reader.Value });
                            break;
                        case XmlNodeType.EntityReference:
                            reader.ResolveEntity();
                            element = elementStack.Peek();
                            element.Nodes.Add(new SvgContentNode() { Content = reader.Value });
                            break;
                    }
                }
                catch (Exception exc)
                {
                    Trace.TraceError(exc.Message);
                }
            }
davescriven's avatar
davescriven committed
326

Eric Domke's avatar
Eric Domke committed
327
328
329
330
331
            if (styles.Any())
            {
                var cssTotal = styles.Select((s) => s.Content).Aggregate((p, c) => p + Environment.NewLine + c);
                var cssParser = new Parser();
                var sheet = cssParser.Parse(cssTotal);
332
333
                AggregateSelectorList aggList;
                IEnumerable<BaseSelector> selectors;
Eric Domke's avatar
Eric Domke committed
334
                IEnumerable<SvgElement> elemsToStyle;
davescriven's avatar
davescriven committed
335

Eric Domke's avatar
Eric Domke committed
336
337
                foreach (var rule in sheet.StyleRules)
                {
338
339
340
341
342
343
344
345
346
347
348
                    aggList = rule.Selector as AggregateSelectorList;
                    if (aggList != null && aggList.Delimiter == ",")
                    {
                        selectors = aggList;
                    }
                    else
                    {
                        selectors = Enumerable.Repeat(rule.Selector, 1);
                    }

                    foreach (var selector in selectors)
davescriven's avatar
davescriven committed
349
                    {
350
351
                        elemsToStyle = svgDocument.QuerySelectorAll(rule.Selector.ToString());
                        foreach (var elem in elemsToStyle)
Eric Domke's avatar
Eric Domke committed
352
                        {
353
354
355
356
                            foreach (var decl in rule.Declarations)
                            {
                                elem.AddStyle(decl.Name, decl.Term.ToString(), rule.Selector.GetSpecificity());
                            }
Eric Domke's avatar
Eric Domke committed
357
                        }
davescriven's avatar
davescriven committed
358
359
                    }
                }
Eric Domke's avatar
Eric Domke committed
360
361
            }

362
            if (svgDocument != null) FlushStyles(svgDocument);
Eric Domke's avatar
Eric Domke committed
363
364
            return svgDocument;
        }
davescriven's avatar
davescriven committed
365

Eric Domke's avatar
Eric Domke committed
366
367
368
369
370
371
        private static void FlushStyles(SvgElement elem)
        {
            elem.FlushStyles();
            foreach (var child in elem.Children)
            {
                FlushStyles(child);
davescriven's avatar
davescriven committed
372
373
374
            }
        }

375
376
377
378
379
        /// <summary>
        /// Opens an SVG document from the specified <see cref="XmlDocument"/>.
        /// </summary>
        /// <param name="document">The <see cref="XmlDocument"/> containing the SVG document XML.</param>
        /// <exception cref="ArgumentNullException">The <paramref name="document"/> parameter cannot be <c>null</c>.</exception>
380
        public static SvgDocument Open(XmlDocument document)
davescriven's avatar
davescriven committed
381
        {
382
383
384
385
386
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }

387
388
            var reader = new SvgNodeReader(document.DocumentElement, null);
            return Open<SvgDocument>(reader);
davescriven's avatar
davescriven committed
389
390
391
392
393
394
395
        }

        public static Bitmap OpenAsBitmap(string path)
        {
            return null;
        }

396
        public static Bitmap OpenAsBitmap(XmlDocument document)
davescriven's avatar
davescriven committed
397
398
399
400
        {
            return null;
        }

401
        /// <summary>
Eric Domke's avatar
Eric Domke committed
402
        /// Renders the <see cref="SvgDocument"/> to the specified <see cref="ISvgRenderer"/>.
403
        /// </summary>
Eric Domke's avatar
Eric Domke committed
404
        /// <param name="renderer">The <see cref="ISvgRenderer"/> to render the document with.</param>
405
        /// <exception cref="ArgumentNullException">The <paramref name="renderer"/> parameter cannot be <c>null</c>.</exception>
Eric Domke's avatar
Eric Domke committed
406
        public void Draw(ISvgRenderer renderer)
davescriven's avatar
davescriven committed
407
        {
408
409
410
411
412
            if (renderer == null)
            {
                throw new ArgumentNullException("renderer");
            }

Eric Domke's avatar
Eric Domke committed
413
            renderer.SetBoundable(this);
414
            this.Render(renderer);
davescriven's avatar
davescriven committed
415
416
        }

417
418
419
420
421
422
423
424
425
426
427
428
        /// <summary>
        /// Renders the <see cref="SvgDocument"/> to the specified <see cref="Graphics"/>.
        /// </summary>
        /// <param name="graphics">The <see cref="Graphics"/> to be rendered to.</param>
        /// <exception cref="ArgumentNullException">The <paramref name="graphics"/> parameter cannot be <c>null</c>.</exception>
        public void Draw(Graphics graphics)
        {
            if (graphics == null)
            {
                throw new ArgumentNullException("graphics");
            }

429
            var renderer = SvgRenderer.FromGraphics(graphics);
Eric Domke's avatar
Eric Domke committed
430
            renderer.SetBoundable(this);
431
            this.Render(renderer);
432
433
        }

434
435
436
437
        /// <summary>
        /// Renders the <see cref="SvgDocument"/> and returns the image as a <see cref="Bitmap"/>.
        /// </summary>
        /// <returns>A <see cref="Bitmap"/> containing the rendered document.</returns>
davescriven's avatar
davescriven committed
438
439
        public virtual Bitmap Draw()
        {
440
            //Trace.TraceInformation("Begin Render");
davescriven's avatar
davescriven committed
441

442
            var size = GetDimensions();
Eric Domke's avatar
Eric Domke committed
443
            var bitmap = new Bitmap((int)Math.Round(size.Width), (int)Math.Round(size.Height));
Eric Domke's avatar
Eric Domke committed
444
            // 	bitmap.SetResolution(300, 300);
Tebjan Halm's avatar
Tebjan Halm committed
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
            try
            {
                Draw(bitmap);
            }
            catch
            {
                bitmap.Dispose();
                throw;
            }

            //Trace.TraceInformation("End Render");
            return bitmap;
        }

        /// <summary>
        /// Renders the <see cref="SvgDocument"/> into a given Bitmap <see cref="Bitmap"/>.
        /// </summary>
        public virtual void Draw(Bitmap bitmap)
        {
            //Trace.TraceInformation("Begin Render");

466
            try
davescriven's avatar
davescriven committed
467
            {
468
469
470
471
472
				using (var renderer = SvgRenderer.FromImage(bitmap))
				{
					renderer.SetBoundable(new GenericBoundable(0, 0, bitmap.Width, bitmap.Height));

					//EO, 2014-12-05: Requested to ensure proper zooming (draw the svg in the bitmap size, ==> proper scaling)
473
474
475
					//EO, 2015-01-09, Added GetDimensions to use its returned size instead of this.Width and this.Height (request of Icarrere).
					SizeF size = this.GetDimensions();
					renderer.ScaleTransform(bitmap.Width / size.Width, bitmap.Height / size.Height);
476

477
					//EO, 2014-12-05: Requested to ensure proper zooming out (reduce size). Otherwise it clip the image.
478
479
480
481
					this.Overflow = SvgOverflow.auto;

					this.Render(renderer);
				}
482
483
484
485
            }
            catch
            {
                throw;
davescriven's avatar
davescriven committed
486
487
            }

488
            //Trace.TraceInformation("End Render");
davescriven's avatar
davescriven committed
489
490
491
492
        }

        public void Write(Stream stream)
        {
Tebjan Halm's avatar
Tebjan Halm committed
493
494
495

            var xmlWriter = new XmlTextWriter(stream, Encoding.UTF8);
            xmlWriter.Formatting = Formatting.Indented;
496

Tebjan Halm's avatar
Tebjan Halm committed
497
            xmlWriter.WriteDocType("svg", "-//W3C//DTD SVG 1.1//EN", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd", null);
sschurig's avatar
sschurig committed
498
499
500
            
            if (!String.IsNullOrEmpty(this.ExternalCSSHref))
                xmlWriter.WriteProcessingInstruction("xml-stylesheet", String.Format("type=\"text/css\" href=\"{0}\"", this.ExternalCSSHref));
Tebjan Halm's avatar
Tebjan Halm committed
501
502
503

            this.WriteElement(xmlWriter);

Tebjan Halm's avatar
Tebjan Halm committed
504
            xmlWriter.Flush();
davescriven's avatar
davescriven committed
505
506
507
508
        }

        public void Write(string path)
        {
Eric Domke's avatar
Eric Domke committed
509
            using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write))
Tebjan Halm's avatar
Tebjan Halm committed
510
511
512
            {
                this.Write(fs);
            }
davescriven's avatar
davescriven committed
513
514
        }
    }
sschurig's avatar
sschurig committed
515
}