SvgDocument.cs 17.4 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
35
36
37
38
39
        }

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

45
                return _idManager;
davescriven's avatar
davescriven committed
46
47
            }
        }
Eric Domke's avatar
Eric Domke committed
48

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

60
61
62
        /// <summary>
        /// Gets or sets the Pixels Per Inch of the rendered image.
        /// </summary>
63
64
65
66
67
        public int Ppi { get; set; }

        #region ITypeDescriptorContext Members

        IContainer ITypeDescriptorContext.Container
davescriven's avatar
davescriven committed
68
        {
69
            get { throw new NotImplementedException(); }
davescriven's avatar
davescriven committed
70
71
        }

72
        object ITypeDescriptorContext.Instance
davescriven's avatar
davescriven committed
73
        {
74
75
76
77
78
79
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
            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
107
108
        }

109
110
111
112
113
114
115
116
117
        /// <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
118
119

        /// <summary>
120
121
122
123
124
125
126
127
128
        /// 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);
        }
129

davescriven's avatar
davescriven committed
130
        /// <summary>
131
        /// Opens the document at the specified path and loads the SVG contents.
davescriven's avatar
davescriven committed
132
133
134
135
        /// </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>
136
        public static T Open<T>(string path) where T : SvgDocument, new()
137
        {
138
            return Open<T>(path, null);
139
140
        }

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

davescriven's avatar
davescriven committed
155
            if (!File.Exists(path))
156
            {
davescriven's avatar
davescriven committed
157
                throw new FileNotFoundException("The specified document cannot be found.", path);
158
            }
davescriven's avatar
davescriven committed
159

160
            return Open<T>(File.OpenRead(path), entities);
davescriven's avatar
davescriven committed
161
162
        }

163
164
165
166
        /// <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>
167
        public static T Open<T>(Stream stream) where T : SvgDocument, new()
davescriven's avatar
davescriven committed
168
        {
169
            return Open<T>(stream, null);
davescriven's avatar
davescriven committed
170
171
        }

Eric Domke's avatar
Eric Domke committed
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

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

193
        /// <summary>
194
        /// Opens an SVG document from the specified <see cref="Stream"/> and adds the specified entities.
195
196
197
        /// </summary>
        /// <param name="stream">The <see cref="Stream"/> containing the SVG document to open.</param>
        /// <param name="entities">Custom entity definitions.</param>
198
        /// <exception cref="ArgumentNullException">The <paramref name="stream"/> parameter cannot be <c>null</c>.</exception>
199
        public static T Open<T>(Stream stream, Dictionary<string, string> entities) where T : SvgDocument, new()
davescriven's avatar
davescriven committed
200
        {
201
202
203
204
205
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

Eric Domke's avatar
Eric Domke committed
206
207
208
209
210
211
            // 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
212

Eric Domke's avatar
Eric Domke committed
213
214
215
216
217
218
219
220
221
222
223
        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
224
            {
Eric Domke's avatar
Eric Domke committed
225
                try
davescriven's avatar
davescriven committed
226
                {
Eric Domke's avatar
Eric Domke committed
227
                    switch (reader.NodeType)
davescriven's avatar
davescriven committed
228
                    {
Eric Domke's avatar
Eric Domke committed
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
                        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
249
                                {
Eric Domke's avatar
Eric Domke committed
250
251
                                    parent.Children.Add(element);
                                    parent.Nodes.Add(element);
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
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
                            }

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

Eric Domke's avatar
Eric Domke committed
303
304
305
306
307
308
            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);
                IEnumerable<SvgElement> elemsToStyle;
davescriven's avatar
davescriven committed
309

Eric Domke's avatar
Eric Domke committed
310
311
312
313
                foreach (var rule in sheet.StyleRules)
                {
                    elemsToStyle = svgDocument.QuerySelectorAll(rule.Selector.ToString());
                    foreach (var elem in elemsToStyle)
davescriven's avatar
davescriven committed
314
                    {
Eric Domke's avatar
Eric Domke committed
315
316
317
318
                        foreach (var decl in rule.Declarations)
                        {
                            elem.AddStyle(decl.Name, decl.Term.ToString(), rule.Selector.GetSpecificity());
                        }
davescriven's avatar
davescriven committed
319
320
                    }
                }
Eric Domke's avatar
Eric Domke committed
321
322
323
324
325
            }

            FlushStyles(svgDocument);
            return svgDocument;
        }
davescriven's avatar
davescriven committed
326

Eric Domke's avatar
Eric Domke committed
327
328
329
330
331
332
        private static void FlushStyles(SvgElement elem)
        {
            elem.FlushStyles();
            foreach (var child in elem.Children)
            {
                FlushStyles(child);
davescriven's avatar
davescriven committed
333
334
335
            }
        }

336
337
338
339
340
        /// <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>
341
        public static SvgDocument Open(XmlDocument document)
davescriven's avatar
davescriven committed
342
        {
343
344
345
346
347
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }

Eric Domke's avatar
Eric Domke committed
348
349
350
351
            using (var stream = new MemoryStream(UTF8Encoding.Default.GetBytes(document.InnerXml)))
            {
                return Open<SvgDocument>(stream, null);
            }
davescriven's avatar
davescriven committed
352
353
354
355
356
357
358
        }

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

359
        public static Bitmap OpenAsBitmap(XmlDocument document)
davescriven's avatar
davescriven committed
360
361
362
363
        {
            return null;
        }

364
365
366
367
        /// <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>
368
        /// <exception cref="ArgumentNullException">The <paramref name="renderer"/> parameter cannot be <c>null</c>.</exception>
369
        public void Draw(SvgRenderer renderer)
davescriven's avatar
davescriven committed
370
        {
371
372
373
374
375
            if (renderer == null)
            {
                throw new ArgumentNullException("renderer");
            }

376
            this.Render(renderer);
davescriven's avatar
davescriven committed
377
378
        }

379
380
381
382
383
384
385
386
387
388
389
390
        /// <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");
            }

391
            this.Render(SvgRenderer.FromGraphics(graphics));
392
393
        }

394
395
396
397
        /// <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
398
399
        public virtual Bitmap Draw()
        {
400
            //Trace.TraceInformation("Begin Render");
davescriven's avatar
davescriven committed
401

402
            var size = GetDimensions();
403
            var bitmap = new Bitmap((int)Math.Ceiling(size.Width), (int)Math.Ceiling(size.Height));
Eric Domke's avatar
Eric Domke committed
404
            // 	bitmap.SetResolution(300, 300);
Tebjan Halm's avatar
Tebjan Halm committed
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
            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");

426
            try
davescriven's avatar
davescriven committed
427
            {
428
                using (var renderer = SvgRenderer.FromImage(bitmap))
429
                {
430
431
432
433
434
                    renderer.TextRenderingHint = TextRenderingHint.AntiAlias;
                    renderer.TextContrast = 1;
                    renderer.PixelOffsetMode = PixelOffsetMode.Half;
                    this.Render(renderer);
                    renderer.Save();
435
436
437
438
439
                }
            }
            catch
            {
                throw;
davescriven's avatar
davescriven committed
440
441
            }

442
            //Trace.TraceInformation("End Render");
davescriven's avatar
davescriven committed
443
444
445
446
        }

        public void Write(Stream stream)
        {
Tebjan Halm's avatar
Tebjan Halm committed
447
448
449

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

Tebjan Halm's avatar
Tebjan Halm committed
451
452
453
454
            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
455
            xmlWriter.Flush();
davescriven's avatar
davescriven committed
456
457
458
459
        }

        public void Write(string path)
        {
Eric Domke's avatar
Eric Domke committed
460
            using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write))
Tebjan Halm's avatar
Tebjan Halm committed
461
462
463
            {
                this.Write(fs);
            }
davescriven's avatar
davescriven committed
464
465
        }
    }
Eric Domke's avatar
Eric Domke committed
466
}