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

25
26
27
        /// <summary>
        /// Initializes a new instance of the <see cref="SvgDocument"/> class.
        /// </summary>
28
        public SvgDocument()
davescriven's avatar
davescriven committed
29
        {
James Welle's avatar
James Welle committed
30
            Ppi = PointsPerInch;
davescriven's avatar
davescriven committed
31
32
        }

33
34
        public Uri BaseUri { get; set; }

davescriven's avatar
davescriven committed
35
36
37
38
39
40
41
        /// <summary>
        /// Gets an <see cref="SvgElementIdManager"/> for this document.
        /// </summary>
        protected internal virtual SvgElementIdManager IdManager
        {
            get
            {
42
                if (_idManager == null)
43
                {
44
                    _idManager = new SvgElementIdManager(this);
45
                }
davescriven's avatar
davescriven committed
46

47
                return _idManager;
davescriven's avatar
davescriven committed
48
49
            }
        }
Eric Domke's avatar
Eric Domke committed
50

51
52
53
54
55
56
57
58
        /// <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
59
            _idManager = manager;
60
        }
davescriven's avatar
davescriven committed
61

62
63
64
        /// <summary>
        /// Gets or sets the Pixels Per Inch of the rendered image.
        /// </summary>
65
        public int Ppi { get; set; }
sschurig's avatar
sschurig committed
66
67
68
69
70
        
        /// <summary>
        /// Gets or sets an external Cascading Style Sheet (CSS)
        /// </summary>
        public string ExternalCSSHref { get; set; }        
71
72
73
74

        #region ITypeDescriptorContext Members

        IContainer ITypeDescriptorContext.Container
davescriven's avatar
davescriven committed
75
        {
76
            get { throw new NotImplementedException(); }
davescriven's avatar
davescriven committed
77
78
        }

79
        object ITypeDescriptorContext.Instance
davescriven's avatar
davescriven committed
80
        {
81
82
83
84
85
86
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
            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
114
115
        }

116
117
118
119
120
121
122
123
124
        /// <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
125
126

        /// <summary>
127
128
129
130
131
132
133
134
135
        /// 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);
        }
136

davescriven's avatar
davescriven committed
137
        /// <summary>
138
        /// Opens the document at the specified path and loads the SVG contents.
davescriven's avatar
davescriven committed
139
140
141
142
        /// </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>
143
        public static T Open<T>(string path) where T : SvgDocument, new()
144
        {
145
            return Open<T>(path, null);
146
147
        }

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

davescriven's avatar
davescriven committed
162
            if (!File.Exists(path))
163
            {
davescriven's avatar
davescriven committed
164
                throw new FileNotFoundException("The specified document cannot be found.", path);
165
            }
davescriven's avatar
davescriven committed
166

167
168
169
            var doc = Open<T>(File.OpenRead(path), entities);
            doc.BaseUri = new Uri(System.IO.Path.GetFullPath(path));
            return doc;
davescriven's avatar
davescriven committed
170
171
        }

172
173
174
175
        /// <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>
176
        public static T Open<T>(Stream stream) where T : SvgDocument, new()
davescriven's avatar
davescriven committed
177
        {
178
            return Open<T>(stream, null);
davescriven's avatar
davescriven committed
179
180
        }

Eric Domke's avatar
Eric Domke committed
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

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

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

Eric Domke's avatar
Eric Domke committed
215
216
217
218
219
220
            // 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
221

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

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

Eric Domke's avatar
Eric Domke committed
312
313
314
315
316
            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);
317
318
                AggregateSelectorList aggList;
                IEnumerable<BaseSelector> selectors;
Eric Domke's avatar
Eric Domke committed
319
                IEnumerable<SvgElement> elemsToStyle;
davescriven's avatar
davescriven committed
320

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

347
            if (svgDocument != null) FlushStyles(svgDocument);
Eric Domke's avatar
Eric Domke committed
348
349
            return svgDocument;
        }
davescriven's avatar
davescriven committed
350

Eric Domke's avatar
Eric Domke committed
351
352
353
354
355
356
        private static void FlushStyles(SvgElement elem)
        {
            elem.FlushStyles();
            foreach (var child in elem.Children)
            {
                FlushStyles(child);
davescriven's avatar
davescriven committed
357
358
359
            }
        }

360
361
362
363
364
        /// <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>
365
        public static SvgDocument Open(XmlDocument document)
davescriven's avatar
davescriven committed
366
        {
367
368
369
370
371
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }

372
373
            var reader = new SvgNodeReader(document.DocumentElement, null);
            return Open<SvgDocument>(reader);
davescriven's avatar
davescriven committed
374
375
376
377
378
379
380
        }

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

381
        public static Bitmap OpenAsBitmap(XmlDocument document)
davescriven's avatar
davescriven committed
382
383
384
385
        {
            return null;
        }

386
387
388
389
        /// <summary>
        /// Renders the <see cref="SvgDocument"/> to the specified <see cref="SvgRenderer"/>.
        /// </summary>
        /// <param name="renderer">The <see cref="SvgRenderer"/> to render the document with.</param>
390
        /// <exception cref="ArgumentNullException">The <paramref name="renderer"/> parameter cannot be <c>null</c>.</exception>
391
        public void Draw(SvgRenderer renderer)
davescriven's avatar
davescriven committed
392
        {
393
394
395
396
397
            if (renderer == null)
            {
                throw new ArgumentNullException("renderer");
            }

398
            renderer.Boundable(this);
399
            this.Render(renderer);
davescriven's avatar
davescriven committed
400
401
        }

402
403
404
405
406
407
408
409
410
411
412
413
        /// <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");
            }

414
415
416
            var renderer = SvgRenderer.FromGraphics(graphics);
            renderer.Boundable(this);
            this.Render(renderer);
417
418
        }

419
420
421
422
        /// <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
423
424
        public virtual Bitmap Draw()
        {
425
            //Trace.TraceInformation("Begin Render");
davescriven's avatar
davescriven committed
426

427
            var size = GetDimensions();
428
            var bitmap = new Bitmap((int)Math.Ceiling(size.Width), (int)Math.Ceiling(size.Height));
Eric Domke's avatar
Eric Domke committed
429
            // 	bitmap.SetResolution(300, 300);
Tebjan Halm's avatar
Tebjan Halm committed
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
            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");

451
            try
davescriven's avatar
davescriven committed
452
            {
453
                using (var renderer = SvgRenderer.FromImage(bitmap))
454
                {
Eric Domke's avatar
Eric Domke committed
455
                    renderer.Boundable(new GenericBoundable(0, 0, bitmap.Width, bitmap.Height));
456
457
458
459
460
                    renderer.TextRenderingHint = TextRenderingHint.AntiAlias;
                    renderer.TextContrast = 1;
                    renderer.PixelOffsetMode = PixelOffsetMode.Half;
                    this.Render(renderer);
                    renderer.Save();
461
462
463
464
465
                }
            }
            catch
            {
                throw;
davescriven's avatar
davescriven committed
466
467
            }

468
            //Trace.TraceInformation("End Render");
davescriven's avatar
davescriven committed
469
470
471
472
        }

        public void Write(Stream stream)
        {
Tebjan Halm's avatar
Tebjan Halm committed
473
474
475

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

Tebjan Halm's avatar
Tebjan Halm committed
477
            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
478
479
480
            
            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
481
482
483

            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
        }
    }
sschurig's avatar
sschurig committed
495
}