SvgDocument.cs 18.6 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

174
175
176
            var doc = Open<T>(File.OpenRead(path), entities);
            doc.BaseUri = new Uri(System.IO.Path.GetFullPath(path));
            return doc;
davescriven's avatar
davescriven committed
177
178
        }

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

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

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

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

Eric Domke's avatar
Eric Domke committed
222
223
224
225
226
227
            // 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
228

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

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

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

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

354
            if (svgDocument != null) FlushStyles(svgDocument);
Eric Domke's avatar
Eric Domke committed
355
356
            return svgDocument;
        }
davescriven's avatar
davescriven committed
357

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

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

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

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

388
        public static Bitmap OpenAsBitmap(XmlDocument document)
davescriven's avatar
davescriven committed
389
390
391
392
        {
            return null;
        }

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

Eric Domke's avatar
Eric Domke committed
405
            renderer.SetBoundable(this);
406
            this.Render(renderer);
davescriven's avatar
davescriven committed
407
408
        }

409
410
411
412
413
414
415
416
417
418
419
420
        /// <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");
            }

421
            var renderer = SvgRenderer.FromGraphics(graphics);
Eric Domke's avatar
Eric Domke committed
422
            renderer.SetBoundable(this);
423
            this.Render(renderer);
424
425
        }

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

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

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

471
            //Trace.TraceInformation("End Render");
davescriven's avatar
davescriven committed
472
473
474
475
        }

        public void Write(Stream stream)
        {
Tebjan Halm's avatar
Tebjan Halm committed
476
477
478

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

Tebjan Halm's avatar
Tebjan Halm committed
480
481
482
483
            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
484
            xmlWriter.Flush();
davescriven's avatar
davescriven committed
485
486
487
488
        }

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