SvgDocument.cs 18.7 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
78
79
80
81
        public int Ppi { get; set; }

        #region ITypeDescriptorContext Members

        IContainer ITypeDescriptorContext.Container
davescriven's avatar
davescriven committed
82
        {
83
            get { throw new NotImplementedException(); }
davescriven's avatar
davescriven committed
84
85
        }

86
        object ITypeDescriptorContext.Instance
davescriven's avatar
davescriven committed
87
        {
88
89
90
91
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
            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
121
122
        }

123
124
125
126
127
128
129
130
131
        /// <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
132
133

        /// <summary>
134
135
136
137
138
139
140
141
142
        /// 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);
        }
143

davescriven's avatar
davescriven committed
144
        /// <summary>
145
        /// Opens the document at the specified path and loads the SVG contents.
davescriven's avatar
davescriven committed
146
147
148
149
        /// </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>
150
        public static T Open<T>(string path) where T : SvgDocument, new()
151
        {
152
            return Open<T>(path, null);
153
154
        }

155
        /// <summary>
156
        /// Opens the document at the specified path and loads the SVG contents.
157
158
159
160
        /// </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>
161
        /// <exception cref="FileNotFoundException">The document at the specified <paramref name="path"/> cannot be found.</exception>
162
        public static T Open<T>(string path, Dictionary<string, string> entities) where T : SvgDocument, new()
davescriven's avatar
davescriven committed
163
        {
164
165
166
167
168
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentNullException("path");
            }

davescriven's avatar
davescriven committed
169
            if (!File.Exists(path))
170
            {
davescriven's avatar
davescriven committed
171
                throw new FileNotFoundException("The specified document cannot be found.", path);
172
            }
davescriven's avatar
davescriven committed
173

Eric Domke's avatar
Eric Domke committed
174
175
176
177
178
179
            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
180
181
        }

182
183
184
185
        /// <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>
186
        public static T Open<T>(Stream stream) where T : SvgDocument, new()
davescriven's avatar
davescriven committed
187
        {
188
            return Open<T>(stream, null);
davescriven's avatar
davescriven committed
189
190
        }

Eric Domke's avatar
Eric Domke committed
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211

        /// <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);
            }
        }

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

Eric Domke's avatar
Eric Domke committed
225
226
227
228
229
230
            // 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
231

Eric Domke's avatar
Eric Domke committed
232
233
234
235
236
237
238
239
240
241
242
        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
243
            {
Eric Domke's avatar
Eric Domke committed
244
                try
davescriven's avatar
davescriven committed
245
                {
Eric Domke's avatar
Eric Domke committed
246
                    switch (reader.NodeType)
davescriven's avatar
davescriven committed
247
                    {
Eric Domke's avatar
Eric Domke committed
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
                        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
268
                                {
Eric Domke's avatar
Eric Domke committed
269
270
                                    parent.Children.Add(element);
                                    parent.Nodes.Add(element);
davescriven's avatar
davescriven committed
271
                                }
Eric Domke's avatar
Eric Domke committed
272
273
274
275
276
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
                            }

                            // 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
321

Eric Domke's avatar
Eric Domke committed
322
323
324
325
326
            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);
327
328
                AggregateSelectorList aggList;
                IEnumerable<BaseSelector> selectors;
Eric Domke's avatar
Eric Domke committed
329
                IEnumerable<SvgElement> elemsToStyle;
davescriven's avatar
davescriven committed
330

Eric Domke's avatar
Eric Domke committed
331
332
                foreach (var rule in sheet.StyleRules)
                {
333
334
335
336
337
338
339
340
341
342
343
                    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
344
                    {
345
346
                        elemsToStyle = svgDocument.QuerySelectorAll(rule.Selector.ToString());
                        foreach (var elem in elemsToStyle)
Eric Domke's avatar
Eric Domke committed
347
                        {
348
349
350
351
                            foreach (var decl in rule.Declarations)
                            {
                                elem.AddStyle(decl.Name, decl.Term.ToString(), rule.Selector.GetSpecificity());
                            }
Eric Domke's avatar
Eric Domke committed
352
                        }
davescriven's avatar
davescriven committed
353
354
                    }
                }
Eric Domke's avatar
Eric Domke committed
355
356
            }

357
            if (svgDocument != null) FlushStyles(svgDocument);
Eric Domke's avatar
Eric Domke committed
358
359
            return svgDocument;
        }
davescriven's avatar
davescriven committed
360

Eric Domke's avatar
Eric Domke committed
361
362
363
364
365
366
        private static void FlushStyles(SvgElement elem)
        {
            elem.FlushStyles();
            foreach (var child in elem.Children)
            {
                FlushStyles(child);
davescriven's avatar
davescriven committed
367
368
369
            }
        }

370
371
372
373
374
        /// <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>
375
        public static SvgDocument Open(XmlDocument document)
davescriven's avatar
davescriven committed
376
        {
377
378
379
380
381
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }

382
383
            var reader = new SvgNodeReader(document.DocumentElement, null);
            return Open<SvgDocument>(reader);
davescriven's avatar
davescriven committed
384
385
386
387
388
389
390
        }

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

391
        public static Bitmap OpenAsBitmap(XmlDocument document)
davescriven's avatar
davescriven committed
392
393
394
395
        {
            return null;
        }

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

Eric Domke's avatar
Eric Domke committed
408
            renderer.SetBoundable(this);
409
            this.Render(renderer);
davescriven's avatar
davescriven committed
410
411
        }

412
413
414
415
416
417
418
419
420
421
422
423
        /// <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");
            }

424
            var renderer = SvgRenderer.FromGraphics(graphics);
Eric Domke's avatar
Eric Domke committed
425
            renderer.SetBoundable(this);
426
            this.Render(renderer);
427
428
        }

429
430
431
432
        /// <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
433
434
        public virtual Bitmap Draw()
        {
435
            //Trace.TraceInformation("Begin Render");
davescriven's avatar
davescriven committed
436

437
            var size = GetDimensions();
Eric Domke's avatar
Eric Domke committed
438
            var bitmap = new Bitmap((int)Math.Round(size.Width), (int)Math.Round(size.Height));
Eric Domke's avatar
Eric Domke committed
439
            // 	bitmap.SetResolution(300, 300);
Tebjan Halm's avatar
Tebjan Halm committed
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
            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");

461
            try
davescriven's avatar
davescriven committed
462
            {
463
                using (var renderer = SvgRenderer.FromImage(bitmap))
464
                {
Eric Domke's avatar
Eric Domke committed
465
                    renderer.SetBoundable(new GenericBoundable(0, 0, bitmap.Width, bitmap.Height));
466
                    this.Render(renderer);
467
468
469
470
471
                }
            }
            catch
            {
                throw;
davescriven's avatar
davescriven committed
472
473
            }

474
            //Trace.TraceInformation("End Render");
davescriven's avatar
davescriven committed
475
476
477
478
        }

        public void Write(Stream stream)
        {
Tebjan Halm's avatar
Tebjan Halm committed
479
480
481

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

Tebjan Halm's avatar
Tebjan Halm committed
483
484
485
486
            xmlWriter.WriteDocType("svg", "-//W3C//DTD SVG 1.1//EN", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd", null);

            this.WriteElement(xmlWriter);

Tebjan Halm's avatar
Tebjan Halm committed
487
            xmlWriter.Flush();
davescriven's avatar
davescriven committed
488
489
490
491
        }

        public void Write(string path)
        {
Eric Domke's avatar
Eric Domke committed
492
            using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write))
Tebjan Halm's avatar
Tebjan Halm committed
493
494
495
            {
                this.Write(fs);
            }
davescriven's avatar
davescriven committed
496
497
        }
    }
Eric Domke's avatar
Eric Domke committed
498
}