From 7c70bd1135d482b7d18aa30b70eb813f8b9d3eb6 Mon Sep 17 00:00:00 2001 From: Eric Domke Date: Sun, 17 Aug 2014 16:56:32 -0400 Subject: [PATCH] Text on a Path & SVG Fonts - Extraction interface for SvgRenderer - Initial support for Text on a Path - Initial support for Svg Fonts - Support for symbol element - Minor bug fixes with image pattern rendering - Additional support for Text whitespace modes --- Source/Basic Shapes/SvgCircle.cs | 4 +- Source/Basic Shapes/SvgEllipse.cs | 4 +- Source/Basic Shapes/SvgImage.cs | 7 +- Source/Basic Shapes/SvgLine.cs | 2 +- Source/Basic Shapes/SvgPolygon.cs | 2 +- Source/Basic Shapes/SvgPolyline.cs | 2 +- Source/Basic Shapes/SvgRectangle.cs | 4 +- Source/Basic Shapes/SvgVisualElement.cs | 66 +- Source/Basic Shapes/SvgVisualElementStyle.cs | 99 ++- Source/Clipping and Masking/ISvgClipable.cs | 12 +- Source/Clipping and Masking/SvgClipPath.cs | 8 +- Source/DataTypes/SvgPoint.cs | 2 +- Source/DataTypes/SvgTextLengthAdjust.cs | 13 + Source/DataTypes/SvgTextPathMethod.cs | 13 + Source/DataTypes/SvgTextPathSpacing.cs | 13 + Source/DataTypes/SvgUnit.cs | 14 +- Source/DataTypes/SvgViewBox.cs | 76 ++ Source/DataTypes/XmlSpaceHandling.cs | 14 + .../Document Structure/SvgDefinitionList.cs | 6 +- .../Document Structure/SvgDocumentMetadata.cs | 6 +- Source/Document Structure/SvgFragment.cs | 81 +- Source/Document Structure/SvgGroup.cs | 27 +- Source/Document Structure/SvgSwitch.cs | 4 +- Source/Document Structure/SvgSymbol.cs | 107 +++ Source/Document Structure/SvgUse.cs | 21 +- Source/Extensibility/SvgForeignObject.cs | 2 +- Source/Filter Effects/ImageBuffer.cs | 10 +- Source/Filter Effects/SvgFilter.cs | 17 +- Source/Painting/ISvgStylable.cs | 2 +- Source/Painting/SvgColourServer.cs | 2 +- Source/Painting/SvgDeferredPaintServer.cs | 2 +- Source/Painting/SvgGradientServer.cs | 4 +- Source/Painting/SvgLinearGradientServer.cs | 10 +- Source/Painting/SvgMarker.cs | 10 +- Source/Painting/SvgPaintServer.cs | 8 +- Source/Painting/SvgPatternServer.cs | 24 +- Source/Painting/SvgRadialGradientServer.cs | 10 +- Source/Paths/SvgPath.cs | 16 +- Source/Rendering/IGraphicsProvider.cs | 13 + Source/Rendering/ISvgRenderer.cs | 29 + Source/Rendering/SvgRenderer.cs | 150 ++++ Source/Svg.csproj | 23 +- Source/SvgAttributeAttribute.cs | 9 +- Source/SvgAttributeCollection.cs | 5 +- Source/SvgDocument.cs | 28 +- Source/SvgElement.cs | 63 +- Source/SvgElementFactory.cs | 3 +- Source/SvgElementIdManager.cs | 25 + Source/SvgRenderer.cs | 197 ----- Source/SvgTextReader.cs | 1 - Source/Text/FontFamily.cs | 237 +++++ Source/Text/GdiFontDefn.cs | 90 ++ Source/Text/IFontDefn.cs | 19 + Source/Text/PathStatistics.cs | 262 ++++++ Source/Text/SvgFont.cs | 55 ++ Source/Text/SvgFontDefn.cs | 130 +++ Source/Text/SvgFontFace.cs | 116 +++ Source/Text/SvgFontFaceSrc.cs | 16 + Source/Text/SvgFontFaceUri.cs | 33 + Source/Text/SvgGlyph.cs | 123 +++ Source/Text/SvgKern.cs | 58 ++ Source/Text/SvgMissingGlyph.cs | 18 + Source/Text/SvgTextBase.cs | 812 +++++++++++++----- Source/Text/SvgTextPath.cs | 97 +++ Source/Text/SvgTextRef.cs | 53 ++ Source/Transforms/ISvgTransformable.cs | 12 +- Tests/SvgW3CTestRunner/View.cs | 1 + Tests/W3CTestSuite/png/__issue-015-01.png | Bin 0 -> 6517 bytes .../{__issue-34-02.png => __issue-034-02.png} | Bin Tests/W3CTestSuite/png/__issue-036-01.png | Bin 0 -> 23538 bytes Tests/W3CTestSuite/png/__issue-064-01.png | Bin 0 -> 1561 bytes Tests/W3CTestSuite/png/__issue-064-02.png | Bin 0 -> 2242 bytes .../{__issue-83-01.png => __issue-083-01.png} | Bin Tests/W3CTestSuite/svg/__issue-015-01.svg | 11 + .../{__issue-34-02.svg => __issue-034-02.svg} | 0 Tests/W3CTestSuite/svg/__issue-036-01.svg | 1 + Tests/W3CTestSuite/svg/__issue-064-01.svg | 42 + Tests/W3CTestSuite/svg/__issue-064-02.svg | 57 ++ .../{__issue-83-01.svg => __issue-083-01.svg} | 0 Tests/W3CTestSuite/svg/text-path-02-b.svg | 8 +- 80 files changed, 2747 insertions(+), 774 deletions(-) create mode 100644 Source/DataTypes/SvgTextLengthAdjust.cs create mode 100644 Source/DataTypes/SvgTextPathMethod.cs create mode 100644 Source/DataTypes/SvgTextPathSpacing.cs create mode 100644 Source/DataTypes/XmlSpaceHandling.cs create mode 100644 Source/Document Structure/SvgSymbol.cs create mode 100644 Source/Rendering/IGraphicsProvider.cs create mode 100644 Source/Rendering/ISvgRenderer.cs create mode 100644 Source/Rendering/SvgRenderer.cs delete mode 100644 Source/SvgRenderer.cs create mode 100644 Source/Text/FontFamily.cs create mode 100644 Source/Text/GdiFontDefn.cs create mode 100644 Source/Text/IFontDefn.cs create mode 100644 Source/Text/PathStatistics.cs create mode 100644 Source/Text/SvgFont.cs create mode 100644 Source/Text/SvgFontDefn.cs create mode 100644 Source/Text/SvgFontFace.cs create mode 100644 Source/Text/SvgFontFaceSrc.cs create mode 100644 Source/Text/SvgFontFaceUri.cs create mode 100644 Source/Text/SvgGlyph.cs create mode 100644 Source/Text/SvgKern.cs create mode 100644 Source/Text/SvgMissingGlyph.cs create mode 100644 Source/Text/SvgTextPath.cs create mode 100644 Source/Text/SvgTextRef.cs create mode 100644 Tests/W3CTestSuite/png/__issue-015-01.png rename Tests/W3CTestSuite/png/{__issue-34-02.png => __issue-034-02.png} (100%) create mode 100644 Tests/W3CTestSuite/png/__issue-036-01.png create mode 100644 Tests/W3CTestSuite/png/__issue-064-01.png create mode 100644 Tests/W3CTestSuite/png/__issue-064-02.png rename Tests/W3CTestSuite/png/{__issue-83-01.png => __issue-083-01.png} (100%) create mode 100644 Tests/W3CTestSuite/svg/__issue-015-01.svg rename Tests/W3CTestSuite/svg/{__issue-34-02.svg => __issue-034-02.svg} (100%) create mode 100644 Tests/W3CTestSuite/svg/__issue-036-01.svg create mode 100644 Tests/W3CTestSuite/svg/__issue-064-01.svg create mode 100644 Tests/W3CTestSuite/svg/__issue-064-02.svg rename Tests/W3CTestSuite/svg/{__issue-83-01.svg => __issue-083-01.svg} (100%) diff --git a/Source/Basic Shapes/SvgCircle.cs b/Source/Basic Shapes/SvgCircle.cs index 93a4fcd..d3fbb6d 100644 --- a/Source/Basic Shapes/SvgCircle.cs +++ b/Source/Basic Shapes/SvgCircle.cs @@ -98,7 +98,7 @@ namespace Svg /// /// Gets the representing this element. /// - public override GraphicsPath Path(SvgRenderer renderer) + public override GraphicsPath Path(ISvgRenderer renderer) { if (this._path == null || this.IsPathDirty) { @@ -117,7 +117,7 @@ namespace Svg /// Renders the circle to the specified object. /// /// The graphics object. - protected override void Render(SvgRenderer renderer) + protected override void Render(ISvgRenderer renderer) { // Don't draw if there is no radius set if (this.Radius.Value > 0.0f) diff --git a/Source/Basic Shapes/SvgEllipse.cs b/Source/Basic Shapes/SvgEllipse.cs index 07f93ad..0ab1d8e 100644 --- a/Source/Basic Shapes/SvgEllipse.cs +++ b/Source/Basic Shapes/SvgEllipse.cs @@ -102,7 +102,7 @@ namespace Svg /// Gets the for this element. /// /// - public override GraphicsPath Path(SvgRenderer renderer) + public override GraphicsPath Path(ISvgRenderer renderer) { if (this._path == null || this.IsPathDirty) { @@ -122,7 +122,7 @@ namespace Svg /// Renders the and contents to the specified object. /// /// The object to render to. - protected override void Render(SvgRenderer renderer) + protected override void Render(ISvgRenderer renderer) { if (this._radiusX.Value > 0.0f && this._radiusY.Value > 0.0f) { diff --git a/Source/Basic Shapes/SvgImage.cs b/Source/Basic Shapes/SvgImage.cs index a8762c9..d6b21a9 100644 --- a/Source/Basic Shapes/SvgImage.cs +++ b/Source/Basic Shapes/SvgImage.cs @@ -94,7 +94,7 @@ namespace Svg /// /// Gets the for this element. /// - public override GraphicsPath Path(SvgRenderer renderer) + public override GraphicsPath Path(ISvgRenderer renderer) { return null; } @@ -102,7 +102,7 @@ namespace Svg /// /// Renders the and contents to the specified object. /// - protected override void Render(SvgRenderer renderer) + protected override void Render(ISvgRenderer renderer) { if (!Visible || !Displayable) return; @@ -120,7 +120,7 @@ namespace Svg RectangleF destRect = destClip; this.PushTransforms(renderer); - renderer.AddClip(new Region(destClip)); + renderer.SetClip(new Region(destClip), CombineMode.Intersect); this.SetClip(renderer); if (AspectRatio != null && AspectRatio.Align != SvgPreserveAspectRatio.none) @@ -223,6 +223,7 @@ namespace Svg if (uri.LocalPath.EndsWith(".svg", StringComparison.InvariantCultureIgnoreCase)) { var doc = SvgDocument.Open(ms); + doc.BaseUri = uri; return doc.Draw(); } else diff --git a/Source/Basic Shapes/SvgLine.cs b/Source/Basic Shapes/SvgLine.cs index 05c12a0..2d66a99 100644 --- a/Source/Basic Shapes/SvgLine.cs +++ b/Source/Basic Shapes/SvgLine.cs @@ -92,7 +92,7 @@ namespace Svg { } - public override System.Drawing.Drawing2D.GraphicsPath Path(SvgRenderer renderer) + public override System.Drawing.Drawing2D.GraphicsPath Path(ISvgRenderer renderer) { if (this._path == null || this.IsPathDirty) { diff --git a/Source/Basic Shapes/SvgPolygon.cs b/Source/Basic Shapes/SvgPolygon.cs index 5c6fc24..b1c3e22 100644 --- a/Source/Basic Shapes/SvgPolygon.cs +++ b/Source/Basic Shapes/SvgPolygon.cs @@ -32,7 +32,7 @@ namespace Svg get { return true; } } - public override GraphicsPath Path(SvgRenderer renderer) + public override GraphicsPath Path(ISvgRenderer renderer) { if (this._path == null || this.IsPathDirty) { diff --git a/Source/Basic Shapes/SvgPolyline.cs b/Source/Basic Shapes/SvgPolyline.cs index c499dd6..2b262f7 100644 --- a/Source/Basic Shapes/SvgPolyline.cs +++ b/Source/Basic Shapes/SvgPolyline.cs @@ -14,7 +14,7 @@ namespace Svg public class SvgPolyline : SvgPolygon { private GraphicsPath _Path; - public override GraphicsPath Path(SvgRenderer renderer) + public override GraphicsPath Path(ISvgRenderer renderer) { if (_Path == null || this.IsPathDirty) { diff --git a/Source/Basic Shapes/SvgRectangle.cs b/Source/Basic Shapes/SvgRectangle.cs index 717e6c6..c9d79c3 100644 --- a/Source/Basic Shapes/SvgRectangle.cs +++ b/Source/Basic Shapes/SvgRectangle.cs @@ -174,7 +174,7 @@ namespace Svg /// /// Gets the for this element. /// - public override GraphicsPath Path(SvgRenderer renderer) + public override GraphicsPath Path(ISvgRenderer renderer) { if (_path == null || IsPathDirty) { @@ -261,7 +261,7 @@ namespace Svg /// /// Renders the and contents to the specified object. /// - protected override void Render(SvgRenderer renderer) + protected override void Render(ISvgRenderer renderer) { if (Width.Value > 0.0f && Height.Value > 0.0f) { diff --git a/Source/Basic Shapes/SvgVisualElement.cs b/Source/Basic Shapes/SvgVisualElement.cs index 5216942..99b14f3 100644 --- a/Source/Basic Shapes/SvgVisualElement.cs +++ b/Source/Basic Shapes/SvgVisualElement.cs @@ -1,6 +1,7 @@ using System; using System.Drawing; using System.Drawing.Drawing2D; +using System.Diagnostics; namespace Svg { @@ -16,7 +17,7 @@ namespace Svg /// /// Gets the for this element. /// - public abstract GraphicsPath Path(SvgRenderer renderer); + public abstract GraphicsPath Path(ISvgRenderer renderer); PointF ISvgBoundable.Location { @@ -104,13 +105,13 @@ namespace Svg /// /// Renders the and contents to the specified object. /// - /// The object to render to. - protected override void Render(SvgRenderer renderer) + /// The object to render to. + protected override void Render(ISvgRenderer renderer) { this.Render(renderer, true); } - private void Render(SvgRenderer renderer, bool renderFilter) + private void Render(ISvgRenderer renderer, bool renderFilter) { if (this.Visible && this.Displayable && this.PushTransforms(renderer) && (!Renderable || this.Path(renderer) != null)) @@ -119,11 +120,20 @@ namespace Svg if (renderFilter && this.Filter != null) { - var filter = this.OwnerDocument.IdManager.GetElementById(this.Filter) as FilterEffects.SvgFilter; + var filterPath = this.Filter; + if (filterPath.ToString().StartsWith("url(")) + { + filterPath = new Uri(filterPath.ToString().Substring(4, filterPath.ToString().Length - 5), UriKind.RelativeOrAbsolute); + } + var filter = this.OwnerDocument.IdManager.GetElementById(filterPath) as FilterEffects.SvgFilter; if (filter != null) { this.PopTransforms(renderer); - filter.ApplyFilter(this, renderer, (r) => this.Render(r, false)); + try + { + filter.ApplyFilter(this, renderer, (r) => this.Render(r, false)); + } + catch (Exception ex) { Debug.Print(ex.ToString()); } renderNormal = false; } } @@ -163,10 +173,10 @@ namespace Svg } /// - /// Renders the fill of the to the specified + /// Renders the fill of the to the specified /// - /// The object to render to. - protected internal virtual void RenderFill(SvgRenderer renderer) + /// The object to render to. + protected internal virtual void RenderFill(ISvgRenderer renderer) { if (this.Fill != null) { @@ -182,12 +192,12 @@ namespace Svg } /// - /// Renders the stroke of the to the specified + /// Renders the stroke of the to the specified /// - /// The object to render to. - protected internal virtual void RenderStroke(SvgRenderer renderer) + /// The object to render to. + protected internal virtual void RenderStroke(ISvgRenderer renderer) { - if (this.Stroke != null) + if (this.Stroke != null && this.Stroke != SvgColourServer.None) { float strokeWidth = this.StrokeWidth.ToDeviceValue(renderer, UnitRenderingType.Other, this); using (var pen = new Pen(this.Stroke.GetBrush(this, renderer, Math.Min(Math.Max(this.StrokeOpacity * this.Opacity, 0), 1)), strokeWidth)) @@ -204,50 +214,50 @@ namespace Svg } /// - /// Sets the clipping region of the specified . + /// Sets the clipping region of the specified . /// - /// The to have its clipping region set. - protected internal virtual void SetClip(SvgRenderer renderer) + /// The to have its clipping region set. + protected internal virtual void SetClip(ISvgRenderer renderer) { if (this.ClipPath != null) { SvgClipPath clipPath = this.OwnerDocument.GetElementById(this.ClipPath.ToString()); - this._previousClip = renderer.Clip; + this._previousClip = renderer.GetClip(); if (clipPath != null) { - renderer.AddClip(clipPath.GetClipRegion(this)); + renderer.SetClip(clipPath.GetClipRegion(this), CombineMode.Intersect); } } } /// - /// Resets the clipping region of the specified back to where it was before the method was called. + /// Resets the clipping region of the specified back to where it was before the method was called. /// - /// The to have its clipping region reset. - protected internal virtual void ResetClip(SvgRenderer renderer) + /// The to have its clipping region reset. + protected internal virtual void ResetClip(ISvgRenderer renderer) { if (this._previousClip != null) { - renderer.Clip = this._previousClip; + renderer.SetClip(this._previousClip); this._previousClip = null; } } /// - /// Sets the clipping region of the specified . + /// Sets the clipping region of the specified . /// - /// The to have its clipping region set. - void ISvgClipable.SetClip(SvgRenderer renderer) + /// The to have its clipping region set. + void ISvgClipable.SetClip(ISvgRenderer renderer) { this.SetClip(renderer); } /// - /// Resets the clipping region of the specified back to where it was before the method was called. + /// Resets the clipping region of the specified back to where it was before the method was called. /// - /// The to have its clipping region reset. - void ISvgClipable.ResetClip(SvgRenderer renderer) + /// The to have its clipping region reset. + void ISvgClipable.ResetClip(ISvgRenderer renderer) { this.ResetClip(renderer); } diff --git a/Source/Basic Shapes/SvgVisualElementStyle.cs b/Source/Basic Shapes/SvgVisualElementStyle.cs index 6433cbb..f2e08df 100644 --- a/Source/Basic Shapes/SvgVisualElementStyle.cs +++ b/Source/Basic Shapes/SvgVisualElementStyle.cs @@ -187,7 +187,7 @@ namespace Svg } /// - /// Refers to the boldness of the font. + /// Refers to the style of the font. /// [SvgAttribute("font-style")] public virtual SvgFontStyle FontStyle @@ -197,7 +197,7 @@ namespace Svg } /// - /// Refers to the boldness of the font. + /// Refers to the varient of the font. /// [SvgAttribute("font-variant")] public virtual SvgFontVariant FontVariant @@ -323,7 +323,7 @@ namespace Svg /// Get the font information based on data stored with the text object or inherited from the parent. /// /// - internal System.Drawing.Font GetFont(SvgRenderer renderer) + internal IFontDefn GetFont(ISvgRenderer renderer) { // Get the font-size float fontSize; @@ -337,62 +337,81 @@ namespace Svg fontSize = fontSizeUnit.ToDeviceValue(renderer, UnitRenderingType.Vertical, this); } - var fontStyle = System.Drawing.FontStyle.Regular; + var family = ValidateFontFamily(this.FontFamily, this.OwnerDocument); + var sFaces = family as IEnumerable; - // Get the font-weight - switch (this.FontWeight) + if (sFaces == null) { - case SvgFontWeight.bold: - case SvgFontWeight.bolder: - case SvgFontWeight.w600: - case SvgFontWeight.w700: - case SvgFontWeight.w800: - case SvgFontWeight.w900: - fontStyle |= System.Drawing.FontStyle.Bold; - break; - } + var fontStyle = System.Drawing.FontStyle.Regular; - // Get the font-style - switch (this.FontStyle) - { - case SvgFontStyle.italic: - case SvgFontStyle.oblique: - fontStyle |= System.Drawing.FontStyle.Italic; - break; - } + // Get the font-weight + switch (this.FontWeight) + { + case SvgFontWeight.bold: + case SvgFontWeight.bolder: + case SvgFontWeight.w600: + case SvgFontWeight.w700: + case SvgFontWeight.w800: + case SvgFontWeight.w900: + fontStyle |= System.Drawing.FontStyle.Bold; + break; + } - // Get the text-decoration - switch (this.TextDecoration) - { - case SvgTextDecoration.lineThrough: - fontStyle |= System.Drawing.FontStyle.Strikeout; - break; - case SvgTextDecoration.underline: - fontStyle |= System.Drawing.FontStyle.Underline; - break; - } + // Get the font-style + switch (this.FontStyle) + { + case SvgFontStyle.italic: + case SvgFontStyle.oblique: + fontStyle |= System.Drawing.FontStyle.Italic; + break; + } - var family = ValidateFontFamily(this.FontFamily); - if (!family.IsStyleAvailable(fontStyle)) + // Get the text-decoration + switch (this.TextDecoration) + { + case SvgTextDecoration.lineThrough: + fontStyle |= System.Drawing.FontStyle.Strikeout; + break; + case SvgTextDecoration.underline: + fontStyle |= System.Drawing.FontStyle.Underline; + break; + } + + var ff = family as FontFamily; + if (!ff.IsStyleAvailable(fontStyle)) + { + // Do Something + } + + // Get the font-family + return new GdiFontDefn(new System.Drawing.Font(ff, fontSize, fontStyle, System.Drawing.GraphicsUnit.Pixel)); + } + else { - // Do Something + var font = sFaces.First().Parent as SvgFont; + if (font == null) + { + var uri = sFaces.First().Descendants().OfType().First().ReferencedElement; + font = OwnerDocument.IdManager.GetElementById(uri) as SvgFont; + } + return new SvgFontDefn(font, fontSize, OwnerDocument.Ppi); } - - // Get the font-family - return new System.Drawing.Font(family, fontSize, fontStyle, System.Drawing.GraphicsUnit.Pixel); } - private static FontFamily ValidateFontFamily(string fontFamilyList) + public static object ValidateFontFamily(string fontFamilyList, SvgDocument doc) { // Split font family list on "," and then trim start and end spaces and quotes. var fontParts = (fontFamilyList ?? "").Split(new[] { ',' }).Select(fontName => fontName.Trim(new[] { '"', ' ', '\'' })); var families = System.Drawing.FontFamily.Families; FontFamily family; + IEnumerable sFaces; // Find a the first font that exists in the list of installed font families. //styles from IE get sent through as lowercase. foreach (var f in fontParts) { + if (doc.FontDefns().TryGetValue(f, out sFaces)) return sFaces; + family = families.FirstOrDefault(ff => ff.Name.ToLower() == f.ToLower()); if (family != null) return family; diff --git a/Source/Clipping and Masking/ISvgClipable.cs b/Source/Clipping and Masking/ISvgClipable.cs index dc604fa..2820ec1 100644 --- a/Source/Clipping and Masking/ISvgClipable.cs +++ b/Source/Clipping and Masking/ISvgClipable.cs @@ -20,14 +20,14 @@ namespace Svg /// SvgClipRule ClipRule { get; set; } /// - /// Sets the clipping region of the specified . + /// Sets the clipping region of the specified . /// - /// The to have its clipping region set. - void SetClip(SvgRenderer renderer); + /// The to have its clipping region set. + void SetClip(ISvgRenderer renderer); /// - /// Resets the clipping region of the specified back to where it was before the method was called. + /// Resets the clipping region of the specified back to where it was before the method was called. /// - /// The to have its clipping region reset. - void ResetClip(SvgRenderer renderer); + /// The to have its clipping region reset. + void ResetClip(ISvgRenderer renderer); } } \ No newline at end of file diff --git a/Source/Clipping and Masking/SvgClipPath.cs b/Source/Clipping and Masking/SvgClipPath.cs index 72bc101..feed3d3 100644 --- a/Source/Clipping and Masking/SvgClipPath.cs +++ b/Source/Clipping and Masking/SvgClipPath.cs @@ -75,7 +75,7 @@ namespace Svg } } - path.AddPath(childPath, false); + if (childPath.PointCount > 0) path.AddPath(childPath, false); } foreach (SvgElement child in element.Children) @@ -108,10 +108,10 @@ namespace Svg } /// - /// Renders the and contents to the specified object. + /// Renders the and contents to the specified object. /// - /// The object to render to. - protected override void Render(SvgRenderer renderer) + /// The object to render to. + protected override void Render(ISvgRenderer renderer) { // Do nothing } diff --git a/Source/DataTypes/SvgPoint.cs b/Source/DataTypes/SvgPoint.cs index e15358d..00d7445 100644 --- a/Source/DataTypes/SvgPoint.cs +++ b/Source/DataTypes/SvgPoint.cs @@ -23,7 +23,7 @@ namespace Svg set { this.y = value; } } - public PointF ToDeviceValue(SvgRenderer renderer, SvgElement owner) + public PointF ToDeviceValue(ISvgRenderer renderer, SvgElement owner) { return SvgUnit.GetDevicePoint(this.X, this.Y, renderer, owner); } diff --git a/Source/DataTypes/SvgTextLengthAdjust.cs b/Source/DataTypes/SvgTextLengthAdjust.cs new file mode 100644 index 0000000..ab68d3a --- /dev/null +++ b/Source/DataTypes/SvgTextLengthAdjust.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + public enum SvgTextLengthAdjust + { + spacing, + spacingAndGlyphs + } +} diff --git a/Source/DataTypes/SvgTextPathMethod.cs b/Source/DataTypes/SvgTextPathMethod.cs new file mode 100644 index 0000000..64aac32 --- /dev/null +++ b/Source/DataTypes/SvgTextPathMethod.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + public enum SvgTextPathMethod + { + align, + stretch + } +} diff --git a/Source/DataTypes/SvgTextPathSpacing.cs b/Source/DataTypes/SvgTextPathSpacing.cs new file mode 100644 index 0000000..e53d8fa --- /dev/null +++ b/Source/DataTypes/SvgTextPathSpacing.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + public enum SvgTextPathSpacing + { + exact, + auto + } +} diff --git a/Source/DataTypes/SvgUnit.cs b/Source/DataTypes/SvgUnit.cs index 9fb4f50..0130f24 100644 --- a/Source/DataTypes/SvgUnit.cs +++ b/Source/DataTypes/SvgUnit.cs @@ -64,7 +64,7 @@ namespace Svg /// /// The container element used as the basis for calculations /// The representation of the current unit in a device value (usually pixels). - public float ToDeviceValue(SvgRenderer renderer, UnitRenderingType renderType, SvgElement owner) + public float ToDeviceValue(ISvgRenderer renderer, UnitRenderingType renderType, SvgElement owner) { // If it's already been calculated if (this._deviceValue.HasValue) @@ -107,7 +107,7 @@ namespace Svg } float points; - Font currFont; + IFontDefn currFont; switch (type) { @@ -158,7 +158,7 @@ namespace Svg break; case SvgUnitType.Percentage: // Can't calculate if there is no style owner - var boundable = (renderer == null ? (owner == null ? null : owner.OwnerDocument) : renderer.Boundable()); + var boundable = (renderer == null ? (owner == null ? null : owner.OwnerDocument) : renderer.GetBoundable()); if (boundable == null) { _deviceValue = value; @@ -193,7 +193,7 @@ namespace Svg return this._deviceValue.Value; } - private Font GetFont(SvgRenderer renderer, SvgElement owner) + private IFontDefn GetFont(ISvgRenderer renderer, SvgElement owner) { if (owner == null) return null; @@ -335,18 +335,18 @@ namespace Svg this._deviceValue = null; } - public static System.Drawing.PointF GetDevicePoint(SvgUnit x, SvgUnit y, SvgRenderer renderer, SvgElement owner) + public static System.Drawing.PointF GetDevicePoint(SvgUnit x, SvgUnit y, ISvgRenderer renderer, SvgElement owner) { return new System.Drawing.PointF(x.ToDeviceValue(renderer, UnitRenderingType.Horizontal, owner), y.ToDeviceValue(renderer, UnitRenderingType.Vertical, owner)); } - public static System.Drawing.PointF GetDevicePointOffset(SvgUnit x, SvgUnit y, SvgRenderer renderer, SvgElement owner) + public static System.Drawing.PointF GetDevicePointOffset(SvgUnit x, SvgUnit y, ISvgRenderer renderer, SvgElement owner) { return new System.Drawing.PointF(x.ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, owner), y.ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, owner)); } - public static System.Drawing.SizeF GetDeviceSize(SvgUnit width, SvgUnit height, SvgRenderer renderer, SvgElement owner) + public static System.Drawing.SizeF GetDeviceSize(SvgUnit width, SvgUnit height, ISvgRenderer renderer, SvgElement owner) { return new System.Drawing.SizeF(width.ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, owner), height.ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, owner)); diff --git a/Source/DataTypes/SvgViewBox.cs b/Source/DataTypes/SvgViewBox.cs index 401140a..62d56ab 100644 --- a/Source/DataTypes/SvgViewBox.cs +++ b/Source/DataTypes/SvgViewBox.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Collections.Generic; using System.Text; using System.Globalization; +using System.Drawing.Drawing2D; namespace Svg { @@ -123,6 +124,81 @@ namespace Svg } #endregion + public void AddViewBoxTransform(SvgAspectRatio aspectRatio, ISvgRenderer renderer, SvgFragment frag) + { + if (this.Equals(SvgViewBox.Empty)) return; + + var width = (frag == null ? this.Width : frag.Width.ToDeviceValue(renderer, UnitRenderingType.Horizontal, frag)); + var height = (frag == null ? this.Height : frag.Height.ToDeviceValue(renderer, UnitRenderingType.Vertical, frag)); + + var fScaleX = width / this.Width; + var fScaleY = height / this.Height; //(this.MinY < 0 ? -1 : 1) * + var fMinX = this.MinX; + var fMinY = this.MinY; + + if (aspectRatio == null) aspectRatio = new SvgAspectRatio(SvgPreserveAspectRatio.xMidYMid, false); + if (aspectRatio.Align != SvgPreserveAspectRatio.none) + { + if (aspectRatio.Slice) + { + fScaleX = Math.Max(fScaleX, fScaleY); + fScaleY = Math.Max(fScaleX, fScaleY); + } + else + { + fScaleX = Math.Min(fScaleX, fScaleY); + fScaleY = Math.Min(fScaleX, fScaleY); + } + float fViewMidX = (this.Width / 2) * fScaleX; + float fViewMidY = (this.Height / 2) * fScaleY; + float fMidX = width / 2; + float fMidY = height / 2; + + switch (aspectRatio.Align) + { + case SvgPreserveAspectRatio.xMinYMin: + break; + case SvgPreserveAspectRatio.xMidYMin: + fMinX += fMidX - fViewMidX; + break; + case SvgPreserveAspectRatio.xMaxYMin: + fMinX += width - this.Width * fScaleX; + break; + case SvgPreserveAspectRatio.xMinYMid: + fMinY += fMidY - fViewMidY; + break; + case SvgPreserveAspectRatio.xMidYMid: + fMinX += fMidX - fViewMidX; + fMinY += fMidY - fViewMidY; + break; + case SvgPreserveAspectRatio.xMaxYMid: + fMinX += width - this.Width * fScaleX; + fMinY += fMidY - fViewMidY; + break; + case SvgPreserveAspectRatio.xMinYMax: + fMinY += height - this.Height * fScaleY; + break; + case SvgPreserveAspectRatio.xMidYMax: + fMinX += fMidX - fViewMidX; + fMinY += height - this.Height * fScaleY; + break; + case SvgPreserveAspectRatio.xMaxYMax: + fMinX += width - this.Width * fScaleX; + fMinY += height - this.Height * fScaleY; + break; + default: + break; + } + } + + var x = (frag == null ? 0 : frag.X.ToDeviceValue(renderer, UnitRenderingType.Horizontal, frag)); + var y = (frag == null ? 0 : frag.Y.ToDeviceValue(renderer, UnitRenderingType.Vertical, frag)); + + renderer.SetClip(new Region(new RectangleF(x, y, width, height)), CombineMode.Intersect); + renderer.ScaleTransform(fScaleX, fScaleY, MatrixOrder.Prepend); + renderer.TranslateTransform(x, y); + renderer.TranslateTransform(-fMinX, -fMinY); + } } internal class SvgViewBoxConverter : TypeConverter diff --git a/Source/DataTypes/XmlSpaceHandling.cs b/Source/DataTypes/XmlSpaceHandling.cs new file mode 100644 index 0000000..1febaf8 --- /dev/null +++ b/Source/DataTypes/XmlSpaceHandling.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + public enum XmlSpaceHandling + { + @default, + inherit, + preserve + } +} diff --git a/Source/Document Structure/SvgDefinitionList.cs b/Source/Document Structure/SvgDefinitionList.cs index 9342773..8f6d966 100644 --- a/Source/Document Structure/SvgDefinitionList.cs +++ b/Source/Document Structure/SvgDefinitionList.cs @@ -18,10 +18,10 @@ namespace Svg } /// - /// Renders the and contents to the specified object. + /// Renders the and contents to the specified object. /// - /// The object to render to. - protected override void Render(SvgRenderer renderer) + /// The object to render to. + protected override void Render(ISvgRenderer renderer) { // Do nothing. Children should NOT be rendered. } diff --git a/Source/Document Structure/SvgDocumentMetadata.cs b/Source/Document Structure/SvgDocumentMetadata.cs index a52cb88..680097f 100644 --- a/Source/Document Structure/SvgDocumentMetadata.cs +++ b/Source/Document Structure/SvgDocumentMetadata.cs @@ -31,10 +31,10 @@ namespace Svg /// - /// Renders the and contents to the specified object. + /// Renders the and contents to the specified object. /// - /// The object to render to. - protected override void Render(SvgRenderer renderer) + /// The object to render to. + protected override void Render(ISvgRenderer renderer) { // Do nothing. Children should NOT be rendered. } diff --git a/Source/Document Structure/SvgFragment.cs b/Source/Document Structure/SvgFragment.cs index f23081f..36f1515 100644 --- a/Source/Document Structure/SvgFragment.cs +++ b/Source/Document Structure/SvgFragment.cs @@ -148,86 +148,13 @@ namespace Svg } /// - /// Applies the required transforms to . + /// Applies the required transforms to . /// - /// The to be transformed. - protected internal override bool PushTransforms(SvgRenderer renderer) + /// The to be transformed. + protected internal override bool PushTransforms(ISvgRenderer renderer) { if (!base.PushTransforms(renderer)) return false; - - if (!this.ViewBox.Equals(SvgViewBox.Empty)) - { - var width = this.Width.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this); - var height = this.Height.ToDeviceValue(renderer, UnitRenderingType.Vertical, this); - - var fScaleX = width / this.ViewBox.Width; - var fScaleY = height / this.ViewBox.Height; - var fMinX = -this.ViewBox.MinX; - var fMinY = -this.ViewBox.MinY; - - if (AspectRatio.Align != SvgPreserveAspectRatio.none) - { - if (AspectRatio.Slice) - { - fScaleX = Math.Max(fScaleX, fScaleY); - fScaleY = Math.Max(fScaleX, fScaleY); - } - else - { - fScaleX = Math.Min(fScaleX, fScaleY); - fScaleY = Math.Min(fScaleX, fScaleY); - } - float fViewMidX = (this.ViewBox.Width / 2) * fScaleX; - float fViewMidY = (this.ViewBox.Height / 2) * fScaleY; - float fMidX = width / 2; - float fMidY = height / 2; - - switch (AspectRatio.Align) - { - case SvgPreserveAspectRatio.xMinYMin: - break; - case SvgPreserveAspectRatio.xMidYMin: - fMinX += fMidX - fViewMidX; - break; - case SvgPreserveAspectRatio.xMaxYMin: - fMinX += width - this.ViewBox.Width * fScaleX; - break; - case SvgPreserveAspectRatio.xMinYMid: - fMinY += fMidY - fViewMidY; - break; - case SvgPreserveAspectRatio.xMidYMid: - fMinX += fMidX - fViewMidX; - fMinY += fMidY - fViewMidY; - break; - case SvgPreserveAspectRatio.xMaxYMid: - fMinX += width - this.ViewBox.Width * fScaleX; - fMinY += fMidY - fViewMidY; - break; - case SvgPreserveAspectRatio.xMinYMax: - fMinY += height - this.ViewBox.Height * fScaleY; - break; - case SvgPreserveAspectRatio.xMidYMax: - fMinX += fMidX - fViewMidX; - fMinY += height - this.ViewBox.Height * fScaleY; - break; - case SvgPreserveAspectRatio.xMaxYMax: - fMinX += width - this.ViewBox.Width * fScaleX; - fMinY += height - this.ViewBox.Height * fScaleY; - break; - default: - break; - } - } - - var x = _x.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this); - var y = _y.ToDeviceValue(renderer, UnitRenderingType.Vertical, this); - - renderer.AddClip(new Region(new RectangleF(x, y, width, height))); - renderer.ScaleTransform(fScaleX, fScaleY, MatrixOrder.Prepend); - renderer.TranslateTransform(x,y); - renderer.TranslateTransform(fMinX, fMinY); - } - + this.ViewBox.AddViewBoxTransform(this.AspectRatio, renderer, this); return true; } diff --git a/Source/Document Structure/SvgGroup.cs b/Source/Document Structure/SvgGroup.cs index f4826f8..63889c6 100644 --- a/Source/Document Structure/SvgGroup.cs +++ b/Source/Document Structure/SvgGroup.cs @@ -9,15 +9,11 @@ namespace Svg [SvgElement("g")] public class SvgGroup : SvgVisualElement { - public SvgGroup() - { - } - /// /// Gets the for this element. /// /// - public override System.Drawing.Drawing2D.GraphicsPath Path(SvgRenderer renderer) + public override System.Drawing.Drawing2D.GraphicsPath Path(ISvgRenderer renderer) { return GetPaths(this, renderer); } @@ -57,26 +53,7 @@ namespace Svg } protected override bool Renderable { get { return false; } } - - ///// - ///// Renders the and contents to the specified object. - ///// - ///// The object to render to. - //protected override void Render(SvgRenderer renderer) - //{ - // if (!Visible || !Displayable) - // return; - - // if (this.PushTransforms(renderer)) - // { - // this.SetClip(renderer); - // base.RenderChildren(renderer); - // this.ResetClip(renderer); - // this.PopTransforms(renderer); - // } - //} - - + public override SvgElement DeepCopy() { return DeepCopy(); diff --git a/Source/Document Structure/SvgSwitch.cs b/Source/Document Structure/SvgSwitch.cs index d370030..d07dabb 100644 --- a/Source/Document Structure/SvgSwitch.cs +++ b/Source/Document Structure/SvgSwitch.cs @@ -17,7 +17,7 @@ namespace Svg /// Gets the for this element. /// /// - public override System.Drawing.Drawing2D.GraphicsPath Path(SvgRenderer renderer) + public override System.Drawing.Drawing2D.GraphicsPath Path(ISvgRenderer renderer) { return GetPaths(this, renderer); } @@ -60,7 +60,7 @@ namespace Svg /// Renders the and contents to the specified object. /// /// The object to render to. - protected override void Render(SvgRenderer renderer) + protected override void Render(ISvgRenderer renderer) { if (!Visible || !Displayable) return; diff --git a/Source/Document Structure/SvgSymbol.cs b/Source/Document Structure/SvgSymbol.cs new file mode 100644 index 0000000..f0ebd2f --- /dev/null +++ b/Source/Document Structure/SvgSymbol.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing; + +namespace Svg.Document_Structure +{ + /// + /// An element used to group SVG shapes. + /// + [SvgElement("symbol")] + public class SvgSymbol : SvgVisualElement + { + + /// + /// Gets or sets the viewport of the element. + /// + /// + [SvgAttribute("viewBox")] + public SvgViewBox ViewBox + { + get { return this.Attributes.GetAttribute("viewBox"); } + set { this.Attributes["viewBox"] = value; } + } + + /// + /// Gets or sets the aspect of the viewport. + /// + /// + [SvgAttribute("preserveAspectRatio")] + public SvgAspectRatio AspectRatio + { + get { return this.Attributes.GetAttribute("preserveAspectRatio"); } + set { this.Attributes["preserveAspectRatio"] = value; } + } + + /// + /// Gets the for this element. + /// + /// + public override System.Drawing.Drawing2D.GraphicsPath Path(ISvgRenderer renderer) + { + return GetPaths(this, renderer); + } + + /// + /// Gets the bounds of the element. + /// + /// The bounds. + public override System.Drawing.RectangleF Bounds + { + get + { + var r = new RectangleF(); + foreach (var c in this.Children) + { + if (c is SvgVisualElement) + { + // First it should check if rectangle is empty or it will return the wrong Bounds. + // This is because when the Rectangle is Empty, the Union method adds as if the first values where X=0, Y=0 + if (r.IsEmpty) + { + r = ((SvgVisualElement)c).Bounds; + } + else + { + var childBounds = ((SvgVisualElement)c).Bounds; + if (!childBounds.IsEmpty) + { + r = RectangleF.Union(r, childBounds); + } + } + } + } + + return r; + } + } + + protected override bool Renderable { get { return false; } } + + /// + /// Applies the required transforms to . + /// + /// The to be transformed. + protected internal override bool PushTransforms(ISvgRenderer renderer) + { + if (!base.PushTransforms(renderer)) return false; + this.ViewBox.AddViewBoxTransform(this.AspectRatio, renderer, null); + return true; + } + + public override SvgElement DeepCopy() + { + return DeepCopy(); + } + + public override SvgElement DeepCopy() + { + var newObj = base.DeepCopy() as SvgSymbol; + if (this.Fill != null) + newObj.Fill = this.Fill.DeepCopy() as SvgPaintServer; + return newObj; + } + } +} diff --git a/Source/Document Structure/SvgUse.cs b/Source/Document Structure/SvgUse.cs index 89dbc54..ba2b6ab 100644 --- a/Source/Document Structure/SvgUse.cs +++ b/Source/Document Structure/SvgUse.cs @@ -35,10 +35,10 @@ namespace Svg } /// - /// Applies the required transforms to . + /// Applies the required transforms to . /// - /// The to be transformed. - protected internal override bool PushTransforms(SvgRenderer renderer) + /// The to be transformed. + protected internal override bool PushTransforms(ISvgRenderer renderer) { if (!base.PushTransforms(renderer)) return false; renderer.TranslateTransform(this.X.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this), @@ -55,7 +55,7 @@ namespace Svg this.Y = 0; } - public override System.Drawing.Drawing2D.GraphicsPath Path(SvgRenderer renderer) + public override System.Drawing.Drawing2D.GraphicsPath Path(ISvgRenderer renderer) { SvgVisualElement element = (SvgVisualElement)this.OwnerDocument.IdManager.GetElementById(this.ReferencedElement); return (element != null) ? element.Path(renderer) : null; @@ -66,20 +66,9 @@ namespace Svg get { return new System.Drawing.RectangleF(); } } - // public override SvgElementCollection Children - // { - // get - // { - // SvgElement element = this.OwnerDocument.IdManager.GetElementById(this.ReferencedElement); - // SvgElementCollection elements = new SvgElementCollection(this, true); - // elements.Add(element); - // return elements; - // } - // } - protected override bool Renderable { get { return false; } } - protected override void Render(SvgRenderer renderer) + protected override void Render(ISvgRenderer renderer) { if (!Visible || !Displayable) return; diff --git a/Source/Extensibility/SvgForeignObject.cs b/Source/Extensibility/SvgForeignObject.cs index d61a954..dd19031 100644 --- a/Source/Extensibility/SvgForeignObject.cs +++ b/Source/Extensibility/SvgForeignObject.cs @@ -17,7 +17,7 @@ namespace Svg /// Gets the for this element. /// /// - public override System.Drawing.Drawing2D.GraphicsPath Path(SvgRenderer renderer) + public override System.Drawing.Drawing2D.GraphicsPath Path(ISvgRenderer renderer) { return GetPaths(this, renderer); } diff --git a/Source/Filter Effects/ImageBuffer.cs b/Source/Filter Effects/ImageBuffer.cs index 5a8ec99..11b721d 100644 --- a/Source/Filter Effects/ImageBuffer.cs +++ b/Source/Filter Effects/ImageBuffer.cs @@ -14,8 +14,8 @@ namespace Svg.FilterEffects private Dictionary _images; private RectangleF _bounds; - private SvgRenderer _renderer; - private Action _renderMethod; + private ISvgRenderer _renderer; + private Action _renderMethod; private float _inflate; public Matrix Transform { get; set; } @@ -41,7 +41,7 @@ namespace Svg.FilterEffects } } - public ImageBuffer(RectangleF bounds, float inflate, SvgRenderer renderer, Action renderMethod) + public ImageBuffer(RectangleF bounds, float inflate, ISvgRenderer renderer, Action renderMethod) { _bounds = bounds; _inflate = inflate; @@ -124,7 +124,7 @@ namespace Svg.FilterEffects } private string ProcessKey(string key) { - if (string.IsNullOrEmpty(key)) return BufferKey; + if (string.IsNullOrEmpty(key)) return _images.ContainsKey(BufferKey) ? BufferKey : SvgFilterPrimitive.SourceGraphic; return key; } @@ -136,7 +136,7 @@ namespace Svg.FilterEffects (int)(_bounds.Height + 2 * _inflate * _bounds.Height + _bounds.Y)); using (var renderer = SvgRenderer.FromImage(graphic)) { - renderer.Boundable(_renderer.Boundable()); + renderer.SetBoundable(_renderer.GetBoundable()); var transform = new Matrix(); transform.Translate(_bounds.Width * _inflate, _bounds.Height * _inflate); renderer.Transform = transform; diff --git a/Source/Filter Effects/SvgFilter.cs b/Source/Filter Effects/SvgFilter.cs index 51c3ccc..2bfc247 100644 --- a/Source/Filter Effects/SvgFilter.cs +++ b/Source/Filter Effects/SvgFilter.cs @@ -80,10 +80,10 @@ namespace Svg.FilterEffects } /// - /// Renders the and contents to the specified object. + /// Renders the and contents to the specified object. /// - /// The object to render to. - protected override void Render(SvgRenderer renderer) + /// The object to render to. + protected override void Render(ISvgRenderer renderer) { base.RenderChildren(renderer); } @@ -109,7 +109,7 @@ namespace Svg.FilterEffects return transformMatrix; } - private RectangleF GetPathBounds(SvgVisualElement element, SvgRenderer renderer, Matrix transform) + private RectangleF GetPathBounds(SvgVisualElement element, ISvgRenderer renderer, Matrix transform) { var bounds = element.Path(renderer).GetBounds(); var pts = new PointF[] { bounds.Location, new PointF(bounds.Right, bounds.Bottom) }; @@ -119,7 +119,7 @@ namespace Svg.FilterEffects Math.Abs(pts[0].X - pts[1].X), Math.Abs(pts[0].Y - pts[1].Y)); } - public void ApplyFilter(SvgVisualElement element, SvgRenderer renderer, Action renderMethod) + public void ApplyFilter(SvgVisualElement element, ISvgRenderer renderer, Action renderMethod) { var inflate = 0.5f; var transform = GetTransform(element); @@ -139,11 +139,10 @@ namespace Svg.FilterEffects var bufferImg = buffer.Buffer; bufferImg.Save(@"C:\test.png"); var imgDraw = RectangleF.Inflate(bounds, inflate * bounds.Width, inflate * bounds.Height); - var prevClip = renderer.Clip; - renderer.Clip = new Region(imgDraw); + var prevClip = renderer.GetClip(); + renderer.SetClip(new Region(imgDraw)); renderer.DrawImage(bufferImg, imgDraw, new RectangleF(bounds.X, bounds.Y, imgDraw.Width, imgDraw.Height), GraphicsUnit.Pixel); - renderer.Clip = prevClip; - //renderer.DrawImage(bufferImg, bounds, bounds, GraphicsUnit.Pixel); + renderer.SetClip(prevClip); } } diff --git a/Source/Painting/ISvgStylable.cs b/Source/Painting/ISvgStylable.cs index 4727416..fb8a19e 100644 --- a/Source/Painting/ISvgStylable.cs +++ b/Source/Painting/ISvgStylable.cs @@ -19,6 +19,6 @@ namespace Svg float StrokeMiterLimit { get; set; } SvgUnitCollection StrokeDashArray { get; set; } SvgUnit StrokeDashOffset { get; set; } - GraphicsPath Path(SvgRenderer renderer); + GraphicsPath Path(ISvgRenderer renderer); } } \ No newline at end of file diff --git a/Source/Painting/SvgColourServer.cs b/Source/Painting/SvgColourServer.cs index 99fdf7c..67fa17d 100644 --- a/Source/Painting/SvgColourServer.cs +++ b/Source/Painting/SvgColourServer.cs @@ -35,7 +35,7 @@ namespace Svg set { this._colour = value; } } - public override Brush GetBrush(SvgVisualElement styleOwner, SvgRenderer renderer, float opacity) + public override Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity) { //is none? if (this == SvgPaintServer.None) return new SolidBrush(System.Drawing.Color.Transparent); diff --git a/Source/Painting/SvgDeferredPaintServer.cs b/Source/Painting/SvgDeferredPaintServer.cs index 22adfc8..cb67518 100644 --- a/Source/Painting/SvgDeferredPaintServer.cs +++ b/Source/Painting/SvgDeferredPaintServer.cs @@ -44,7 +44,7 @@ namespace Svg } } - public override System.Drawing.Brush GetBrush(SvgVisualElement styleOwner, SvgRenderer renderer, float opacity) + public override System.Drawing.Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity) { EnsureServer(styleOwner); return _concreteServer.GetBrush(styleOwner, renderer, opacity); diff --git a/Source/Painting/SvgGradientServer.cs b/Source/Painting/SvgGradientServer.cs index 1552869..aea4892 100644 --- a/Source/Painting/SvgGradientServer.cs +++ b/Source/Painting/SvgGradientServer.cs @@ -130,7 +130,7 @@ namespace Svg /// /// The parent . /// The opacity of the colour blend. - protected ColorBlend GetColorBlend(SvgRenderer renderer, float opacity, bool radial) + protected ColorBlend GetColorBlend(ISvgRenderer renderer, float opacity, bool radial) { int colourBlends = this.Stops.Count; bool insertStart = false; @@ -184,7 +184,7 @@ namespace Svg for (int i = 0; i < colourBlends; i++) { var currentStop = this.Stops[radial ? this.Stops.Count - 1 - actualStops : actualStops]; - var boundWidth = renderer.Boundable().Bounds.Width; + var boundWidth = renderer.GetBoundable().Bounds.Width; mergedOpacity = opacity * currentStop.Opacity; position = diff --git a/Source/Painting/SvgLinearGradientServer.cs b/Source/Painting/SvgLinearGradientServer.cs index cc791ef..f419e76 100644 --- a/Source/Painting/SvgLinearGradientServer.cs +++ b/Source/Painting/SvgLinearGradientServer.cs @@ -79,7 +79,7 @@ namespace Svg Y2 = new SvgUnit(SvgUnitType.Percentage, 0F); } - public override Brush GetBrush(SvgVisualElement renderingElement, SvgRenderer renderer, float opacity) + public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity) { LoadStops(renderingElement); if (IsInvalid) @@ -89,7 +89,7 @@ namespace Svg try { - if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.Boundable(renderingElement); + if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.SetBoundable(renderingElement); var specifiedStart = CalculateStart(renderer); var specifiedEnd = CalculateEnd(renderer); @@ -116,12 +116,12 @@ namespace Svg } } - private PointF CalculateStart(SvgRenderer renderer) + private PointF CalculateStart(ISvgRenderer renderer) { return TransformPoint(SvgUnit.GetDevicePointOffset(this.X1, this.Y1, renderer, this)); } - private PointF CalculateEnd(SvgRenderer renderer) + private PointF CalculateEnd(ISvgRenderer renderer) { return TransformPoint(SvgUnit.GetDevicePointOffset(this.X2, this.Y2, renderer, this)); } @@ -181,7 +181,7 @@ namespace Svg return new GradientPoints(effectiveStart, effectiveEnd); } - private ColorBlend CalculateColorBlend(SvgRenderer renderer, float opacity, PointF specifiedStart, PointF effectiveStart, PointF specifiedEnd, PointF effectiveEnd) + private ColorBlend CalculateColorBlend(ISvgRenderer renderer, float opacity, PointF specifiedStart, PointF effectiveStart, PointF specifiedEnd, PointF effectiveEnd) { var colorBlend = GetColorBlend(renderer, opacity, false); diff --git a/Source/Painting/SvgMarker.cs b/Source/Painting/SvgMarker.cs index 12b4f80..a959e3d 100644 --- a/Source/Painting/SvgMarker.cs +++ b/Source/Painting/SvgMarker.cs @@ -87,7 +87,7 @@ namespace Svg Overflow = SvgOverflow.hidden; } - public override System.Drawing.Drawing2D.GraphicsPath Path(SvgRenderer renderer) + public override System.Drawing.Drawing2D.GraphicsPath Path(ISvgRenderer renderer) { var path = this.Children.FirstOrDefault(x => x is SvgPath); if (path != null) @@ -131,7 +131,7 @@ namespace Svg /// /// /// - public void RenderMarker(SvgRenderer pRenderer, SvgPath pOwner, PointF pRefPoint, PointF pMarkerPoint1, PointF pMarkerPoint2) + public void RenderMarker(ISvgRenderer pRenderer, SvgPath pOwner, PointF pRefPoint, PointF pMarkerPoint1, PointF pMarkerPoint2) { float xDiff = pMarkerPoint2.X - pMarkerPoint1.X; float yDiff = pMarkerPoint2.Y - pMarkerPoint1.Y; @@ -148,7 +148,7 @@ namespace Svg /// /// /// - public void RenderMarker(SvgRenderer pRenderer, SvgPath pOwner, PointF pRefPoint, PointF pMarkerPoint1, PointF pMarkerPoint2, PointF pMarkerPoint3) + public void RenderMarker(ISvgRenderer pRenderer, SvgPath pOwner, PointF pRefPoint, PointF pMarkerPoint1, PointF pMarkerPoint2, PointF pMarkerPoint3) { float xDiff = pMarkerPoint2.X - pMarkerPoint1.X; float yDiff = pMarkerPoint2.Y - pMarkerPoint1.Y; @@ -168,7 +168,7 @@ namespace Svg /// /// /// - private void RenderPart2(float fAngle, SvgRenderer pRenderer, SvgPath pOwner, PointF pMarkerPoint) + private void RenderPart2(float fAngle, ISvgRenderer pRenderer, SvgPath pOwner, PointF pMarkerPoint) { Pen pRenderPen = CreatePen(pOwner, pRenderer); @@ -216,7 +216,7 @@ namespace Svg /// /// /// - private Pen CreatePen(SvgPath pPath, SvgRenderer renderer) + private Pen CreatePen(SvgPath pPath, ISvgRenderer renderer) { Brush pBrush = pPath.Stroke.GetBrush(this, renderer, Opacity); switch (MarkerUnits) diff --git a/Source/Painting/SvgPaintServer.cs b/Source/Painting/SvgPaintServer.cs index 11c342e..417024c 100644 --- a/Source/Painting/SvgPaintServer.cs +++ b/Source/Painting/SvgPaintServer.cs @@ -26,10 +26,10 @@ namespace Svg } /// - /// Renders the and contents to the specified object. + /// Renders the and contents to the specified object. /// - /// The object to render to. - protected override void Render(SvgRenderer renderer) + /// The object to render to. + protected override void Render(ISvgRenderer renderer) { // Never render paint servers or their children } @@ -39,7 +39,7 @@ namespace Svg /// /// The owner . /// The opacity of the brush. - public abstract Brush GetBrush(SvgVisualElement styleOwner, SvgRenderer renderer, float opacity); + public abstract Brush GetBrush(SvgVisualElement styleOwner, ISvgRenderer renderer, float opacity); /// /// Returns a that represents the current . diff --git a/Source/Painting/SvgPatternServer.cs b/Source/Painting/SvgPatternServer.cs index 9afa695..48adb39 100644 --- a/Source/Painting/SvgPatternServer.cs +++ b/Source/Painting/SvgPatternServer.cs @@ -20,8 +20,8 @@ namespace Svg private SvgUnit _x; private SvgUnit _y; private SvgViewBox _viewBox; - private SvgCoordinateUnits _patternUnits; - private SvgCoordinateUnits _patternContentUnits; + private SvgCoordinateUnits _patternUnits = SvgCoordinateUnits.ObjectBoundingBox; + private SvgCoordinateUnits _patternContentUnits = SvgCoordinateUnits.UserSpaceOnUse; [SvgAttribute("overflow")] public SvgOverflow Overflow @@ -129,7 +129,7 @@ namespace Svg /// /// The owner . /// The opacity of the brush. - public override Brush GetBrush(SvgVisualElement renderingElement, SvgRenderer renderer, float opacity) + public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity) { // If there aren't any children, return null if (this.Children.Count == 0) @@ -141,7 +141,7 @@ namespace Svg try { - if (this.PatternUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.Boundable(renderingElement); + if (this.PatternUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.SetBoundable(renderingElement); float width = this._width.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this); float height = this._height.ToDeviceValue(renderer, UnitRenderingType.Vertical, this); @@ -153,11 +153,7 @@ namespace Svg float x = this._x.ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, this); float y = this._y.ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, this); - patternMatrix.Translate(x + -1.0f, y + -1.0f); - } - else - { - patternMatrix.Translate(-1, -1); + patternMatrix.Translate(x, y); } if (this.ViewBox.Height > 0 || this.ViewBox.Width > 0) @@ -167,20 +163,16 @@ namespace Svg } Bitmap image = new Bitmap((int)width, (int)height); - using (SvgRenderer iRenderer = SvgRenderer.FromImage(image)) + using (var iRenderer = SvgRenderer.FromImage(image)) { - iRenderer.Boundable((_patternContentUnits == SvgCoordinateUnits.ObjectBoundingBox) ? new GenericBoundable(0, 0, width, height) : renderer.Boundable()); + iRenderer.SetBoundable((_patternContentUnits == SvgCoordinateUnits.ObjectBoundingBox) ? new GenericBoundable(0, 0, width, height) : renderer.GetBoundable()); iRenderer.Transform = patternMatrix; - iRenderer.CompositingQuality = CompositingQuality.HighQuality; iRenderer.SmoothingMode = SmoothingMode.AntiAlias; - iRenderer.PixelOffsetMode = PixelOffsetMode.Half; - + foreach (SvgElement child in this.Children) { child.RenderElement(iRenderer); } - - iRenderer.Save(); } image.Save(string.Format(@"C:\test{0:D3}.png", imgNumber++)); diff --git a/Source/Painting/SvgRadialGradientServer.cs b/Source/Painting/SvgRadialGradientServer.cs index 5a1a6cd..240f87e 100644 --- a/Source/Painting/SvgRadialGradientServer.cs +++ b/Source/Painting/SvgRadialGradientServer.cs @@ -98,16 +98,16 @@ namespace Svg private object _lockObj = new Object(); - public override Brush GetBrush(SvgVisualElement renderingElement, SvgRenderer renderer, float opacity) + public override Brush GetBrush(SvgVisualElement renderingElement, ISvgRenderer renderer, float opacity) { LoadStops(renderingElement); try { - if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.Boundable(renderingElement); + if (this.GradientUnits == SvgCoordinateUnits.ObjectBoundingBox) renderer.SetBoundable(renderingElement); // Calculate the path and transform it appropriately - var origin = renderer.Boundable().Location; + var origin = renderer.GetBoundable().Location; var center = new PointF(origin.X + CenterX.ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, this), origin.Y + CenterY.ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, this)); var specifiedRadius = Radius.ToDeviceValue(renderer, UnitRenderingType.Other, this); @@ -181,7 +181,7 @@ namespace Svg return bounds.Height / (points[2].Y - points[1].Y); } - private PointF CalculateFocalPoint(SvgRenderer renderer, PointF origin) + private PointF CalculateFocalPoint(ISvgRenderer renderer, PointF origin) { var deviceFocalX = origin.X + FocalX.ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, this); var deviceFocalY = origin.Y + FocalY.ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, this); @@ -203,7 +203,7 @@ namespace Svg return path; } - private ColorBlend CalculateColorBlend(SvgRenderer renderer, float opacity, float scale, out float outScale) + private ColorBlend CalculateColorBlend(ISvgRenderer renderer, float opacity, float scale, out float outScale) { var colorBlend = GetColorBlend(renderer, opacity, true); float newScale; diff --git a/Source/Paths/SvgPath.cs b/Source/Paths/SvgPath.cs index 95757d2..25de7c2 100644 --- a/Source/Paths/SvgPath.cs +++ b/Source/Paths/SvgPath.cs @@ -38,9 +38,9 @@ namespace Svg /// Gets or sets the length of the path. /// [SvgAttribute("pathLength")] - public int PathLength + public float PathLength { - get { return this.Attributes.GetAttribute("pathLength"); } + get { return this.Attributes.GetAttribute("pathLength"); } set { this.Attributes["pathLength"] = value; } } @@ -81,7 +81,7 @@ namespace Svg /// /// Gets the for this element. /// - public override GraphicsPath Path(SvgRenderer renderer) + public override GraphicsPath Path(ISvgRenderer renderer) { if (this._path == null || this.IsPathDirty) { @@ -131,15 +131,15 @@ namespace Svg } /// - /// Renders the stroke of the to the specified + /// Renders the stroke of the to the specified /// - /// The object to render to. - protected internal override void RenderStroke(SvgRenderer renderer) + /// The object to render to. + protected internal override void RenderStroke(ISvgRenderer renderer) { - if (this.Stroke != null) + if (this.Stroke != null && this.Stroke != SvgColourServer.None) { float strokeWidth = this.StrokeWidth.ToDeviceValue(renderer, UnitRenderingType.Other, this); - using (Pen pen = new Pen(this.Stroke.GetBrush(this, renderer, this.StrokeOpacity), strokeWidth)) + using (Pen pen = new Pen(this.Stroke.GetBrush(this, renderer, this.StrokeOpacity * this.Opacity), strokeWidth)) { if (this.StrokeDashArray != null && this.StrokeDashArray.Count > 0) { diff --git a/Source/Rendering/IGraphicsProvider.cs b/Source/Rendering/IGraphicsProvider.cs new file mode 100644 index 0000000..743c50f --- /dev/null +++ b/Source/Rendering/IGraphicsProvider.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing; + +namespace Svg +{ + public interface IGraphicsProvider + { + Graphics GetGraphics(); + } +} diff --git a/Source/Rendering/ISvgRenderer.cs b/Source/Rendering/ISvgRenderer.cs new file mode 100644 index 0000000..0488abd --- /dev/null +++ b/Source/Rendering/ISvgRenderer.cs @@ -0,0 +1,29 @@ +using System; +using System.Drawing.Drawing2D; +using System.Drawing; +using System.Collections.Generic; + +namespace Svg +{ + public interface ISvgRenderer : IDisposable + { + float DpiY { get; } + void DrawImage(Image image, RectangleF destRect, RectangleF srcRect, GraphicsUnit graphicsUnit); + void DrawImageUnscaled(Image image, Point location); + void DrawPath(Pen pen, GraphicsPath path); + void FillPath(Brush brush, GraphicsPath path); + float FontBaselineOffset(IFontDefn font); + ISvgBoundable GetBoundable(); + Region GetClip(); + IList MeasureCharacters(string text, IFontDefn font); + SizeF MeasureString(string text, IFontDefn font); + ISvgBoundable PopBoundable(); + void RotateTransform(float fAngle, MatrixOrder order = MatrixOrder.Append); + void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Append); + void SetBoundable(ISvgBoundable boundable); + void SetClip(Region region, CombineMode combineMode = CombineMode.Replace); + SmoothingMode SmoothingMode { get; set; } + Matrix Transform { get; set; } + void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrder.Append); + } +} diff --git a/Source/Rendering/SvgRenderer.cs b/Source/Rendering/SvgRenderer.cs new file mode 100644 index 0000000..2c03b01 --- /dev/null +++ b/Source/Rendering/SvgRenderer.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Text; + +namespace Svg +{ + /// + /// Convenience wrapper around a graphics object + /// + public sealed class SvgRenderer : IDisposable, IGraphicsProvider, ISvgRenderer + { + private Graphics _innerGraphics; + private Stack _boundables = new Stack(); + + public void SetBoundable(ISvgBoundable boundable) + { + _boundables.Push(boundable); + } + public ISvgBoundable GetBoundable() + { + return _boundables.Peek(); + } + public ISvgBoundable PopBoundable() + { + return _boundables.Pop(); + } + + public float DpiY + { + get { return _innerGraphics.DpiY; } + } + + /// + /// Initializes a new instance of the class. + /// + private SvgRenderer(Graphics graphics) + { + this._innerGraphics = graphics; + } + + public void DrawImage(Image image, RectangleF destRect, RectangleF srcRect, GraphicsUnit graphicsUnit) + { + _innerGraphics.DrawImage(image, destRect, srcRect, graphicsUnit); + } + public void DrawImageUnscaled(Image image, Point location) + { + this._innerGraphics.DrawImageUnscaled(image, location); + } + public void DrawPath(Pen pen, GraphicsPath path) + { + this._innerGraphics.DrawPath(pen, path); + } + public void FillPath(Brush brush, GraphicsPath path) + { + this._innerGraphics.FillPath(brush, path); + } + public Region GetClip() + { + return this._innerGraphics.Clip; + } + public void RotateTransform(float fAngle, MatrixOrder order = MatrixOrder.Append) + { + this._innerGraphics.RotateTransform(fAngle, order); + } + public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Append) + { + this._innerGraphics.ScaleTransform(sx, sy, order); + } + public void SetClip(Region region, CombineMode combineMode = CombineMode.Replace) + { + this._innerGraphics.SetClip(region, combineMode); + } + public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrder.Append) + { + this._innerGraphics.TranslateTransform(dx, dy, order); + } + + + + public SmoothingMode SmoothingMode + { + get { return this._innerGraphics.SmoothingMode; } + set { this._innerGraphics.SmoothingMode = value; } + } + + public Matrix Transform + { + get { return this._innerGraphics.Transform; } + set { this._innerGraphics.Transform = value; } + } + + public void Dispose() + { + this._innerGraphics.Dispose(); + } + + public float FontBaselineOffset(IFontDefn font) + { + return font.Ascent(this); + } + + public IList MeasureCharacters(string text, IFontDefn font) + { + return font.MeasureCharacters(this, text); + } + + public SizeF MeasureString(string text, IFontDefn font) + { + return font.MeasureString(this, text); + } + + Graphics IGraphicsProvider.GetGraphics() + { + return _innerGraphics; + } + + /// + /// Creates a new from the specified . + /// + /// from which to create the new . + public static ISvgRenderer FromImage(Image image) + { + var g = Graphics.FromImage(image); + g.TextRenderingHint = TextRenderingHint.AntiAlias; + g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half; + g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; + g.TextContrast = 1; + return new SvgRenderer(g); + } + + /// + /// Creates a new from the specified . + /// + /// The to create the renderer from. + public static ISvgRenderer FromGraphics(Graphics graphics) + { + return new SvgRenderer(graphics); + } + + public static ISvgRenderer FromNull() + { + var img = new Bitmap(1, 1); + return SvgRenderer.FromImage(img); + } + } +} \ No newline at end of file diff --git a/Source/Svg.csproj b/Source/Svg.csproj index 7e2ee28..56a1bb4 100644 --- a/Source/Svg.csproj +++ b/Source/Svg.csproj @@ -101,8 +101,15 @@ + + + + + + + @@ -235,7 +242,7 @@ - + @@ -276,10 +283,24 @@ + + + + + + + + + + + + + + diff --git a/Source/SvgAttributeAttribute.cs b/Source/SvgAttributeAttribute.cs index cc8c72e..eca8f18 100644 --- a/Source/SvgAttributeAttribute.cs +++ b/Source/SvgAttributeAttribute.cs @@ -15,13 +15,14 @@ namespace Svg /// /// Gets a containing the XLink namespace (http://www.w3.org/1999/xlink). /// - public const string SVG_NAMESPACE = "http://www.w3.org/2000/svg"; + public const string SvgNamespace = "http://www.w3.org/2000/svg"; public const string XLinkPrefix = "xlink"; public const string XLinkNamespace = "http://www.w3.org/1999/xlink"; + public const string XmlNamespace = "http://www.w3.org/XML/1998/namespace"; public static readonly List> Namespaces = new List>() { - new KeyValuePair("", SVG_NAMESPACE), + new KeyValuePair("", SvgNamespace), new KeyValuePair(XLinkPrefix, XLinkNamespace) }; private string _name; @@ -55,7 +56,7 @@ namespace Svg { get { - if (_namespace == SVG_NAMESPACE) + if (_namespace == SvgNamespace) return _name; return Namespaces.First(x => x.Value == _namespace).Key + ":" + _name; } @@ -93,7 +94,7 @@ namespace Svg internal SvgAttributeAttribute(string name) { this._name = name; - this._namespace = SVG_NAMESPACE; + this._namespace = SvgNamespace; } /// diff --git a/Source/SvgAttributeCollection.cs b/Source/SvgAttributeCollection.cs index db60897..39e85f6 100644 --- a/Source/SvgAttributeCollection.cs +++ b/Source/SvgAttributeCollection.cs @@ -89,8 +89,9 @@ namespace Svg (value is SvgFontWeight && (SvgFontWeight)value == SvgFontWeight.inherit) || (value is SvgTextAnchor && (SvgTextAnchor)value == SvgTextAnchor.inherit) || (value is SvgFontVariant && (SvgFontVariant)value == SvgFontVariant.inherit) || - (value is SvgTextDecoration && (SvgTextDecoration)value == SvgTextDecoration.inherit) || - (value == "inherit") + (value is SvgTextDecoration && (SvgTextDecoration)value == SvgTextDecoration.inherit) || + (value is XmlSpaceHandling && (XmlSpaceHandling)value == XmlSpaceHandling.inherit) || + (value is string && (string)value == "inherit") ); } diff --git a/Source/SvgDocument.cs b/Source/SvgDocument.cs index c5fd0dc..14f1ef8 100644 --- a/Source/SvgDocument.cs +++ b/Source/SvgDocument.cs @@ -22,6 +22,18 @@ namespace Svg public static readonly int PointsPerInch = 96; private SvgElementIdManager _idManager; + private Dictionary> _fontDefns = null; + internal Dictionary> FontDefns() + { + if (_fontDefns == null) + { + _fontDefns = (from f in Descendants().OfType() + group f by f.FontFamily into family + select family).ToDictionary(f => f.Key, f => (IEnumerable)f); + } + return _fontDefns; + } + /// /// Initializes a new instance of the class. /// @@ -379,18 +391,18 @@ namespace Svg } /// - /// Renders the to the specified . + /// Renders the to the specified . /// - /// The to render the document with. + /// The to render the document with. /// The parameter cannot be null. - public void Draw(SvgRenderer renderer) + public void Draw(ISvgRenderer renderer) { if (renderer == null) { throw new ArgumentNullException("renderer"); } - renderer.Boundable(this); + renderer.SetBoundable(this); this.Render(renderer); } @@ -407,7 +419,7 @@ namespace Svg } var renderer = SvgRenderer.FromGraphics(graphics); - renderer.Boundable(this); + renderer.SetBoundable(this); this.Render(renderer); } @@ -447,12 +459,8 @@ namespace Svg { using (var renderer = SvgRenderer.FromImage(bitmap)) { - renderer.Boundable(new GenericBoundable(0, 0, bitmap.Width, bitmap.Height)); - renderer.TextRenderingHint = TextRenderingHint.AntiAlias; - renderer.TextContrast = 1; - renderer.PixelOffsetMode = PixelOffsetMode.Half; + renderer.SetBoundable(new GenericBoundable(0, 0, bitmap.Width, bitmap.Height)); this.Render(renderer); - renderer.Save(); } } catch diff --git a/Source/SvgElement.cs b/Source/SvgElement.cs index b2ceb6c..d808b9c 100644 --- a/Source/SvgElement.cs +++ b/Source/SvgElement.cs @@ -255,13 +255,13 @@ namespace Svg } /// - /// Applies the required transforms to . + /// Applies the required transforms to . /// - /// The to be transformed. - protected internal virtual bool PushTransforms(SvgRenderer renderer) + /// The to be transformed. + protected internal virtual bool PushTransforms(ISvgRenderer renderer) { _graphicsMatrix = renderer.Transform; - _graphicsClip = renderer.Clip; + _graphicsClip = renderer.GetClip(); // Return if there are no transforms if (this.Transforms == null || this.Transforms.Count == 0) @@ -283,10 +283,10 @@ namespace Svg } /// - /// Removes any previously applied transforms from the specified . + /// Removes any previously applied transforms from the specified . /// - /// The that should have transforms removed. - protected internal virtual void PopTransforms(SvgRenderer renderer) + /// The that should have transforms removed. + protected internal virtual void PopTransforms(ISvgRenderer renderer) { renderer.Transform = _graphicsMatrix; _graphicsMatrix = null; @@ -295,19 +295,19 @@ namespace Svg } /// - /// Applies the required transforms to . + /// Applies the required transforms to . /// - /// The to be transformed. - void ISvgTransformable.PushTransforms(SvgRenderer renderer) + /// The to be transformed. + void ISvgTransformable.PushTransforms(ISvgRenderer renderer) { this.PushTransforms(renderer); } /// - /// Removes any previously applied transforms from the specified . + /// Removes any previously applied transforms from the specified . /// - /// The that should have transforms removed. - void ISvgTransformable.PopTransforms(SvgRenderer renderer) + /// The that should have transforms removed. + void ISvgTransformable.PopTransforms(ISvgRenderer renderer) { this.PopTransforms(renderer); } @@ -344,6 +344,17 @@ namespace Svg } } + /// + /// Gets or sets the text anchor. + /// + /// The text anchor. + [SvgAttribute("space", SvgAttributeAttribute.XmlNamespace)] + public virtual XmlSpaceHandling SpaceHandling + { + get { return (this.Attributes["space"] == null) ? XmlSpaceHandling.inherit : (XmlSpaceHandling)this.Attributes["space"]; } + set { this.Attributes["space"] = value; } + } + public void SetAndForceUniqueID(string value, bool autoForceUniqueID = true, Action logElementOldIDNewID = null) { // Don't do anything if it hasn't changed @@ -469,10 +480,10 @@ namespace Svg /// - /// Renders this element to the . + /// Renders this element to the . /// - /// The that the element should use to render itself. - public void RenderElement(SvgRenderer renderer) + /// The that the element should use to render itself. + public void RenderElement(ISvgRenderer renderer) { this.Render(renderer); } @@ -632,10 +643,10 @@ namespace Svg } /// - /// Renders the and contents to the specified object. + /// Renders the and contents to the specified object. /// - /// The object to render to. - protected virtual void Render(SvgRenderer renderer) + /// The object to render to. + protected virtual void Render(ISvgRenderer renderer) { this.PushTransforms(renderer); this.RenderChildren(renderer); @@ -645,8 +656,8 @@ namespace Svg /// /// Renders the children of this . /// - /// The to render the child s to. - protected virtual void RenderChildren(SvgRenderer renderer) + /// The to render the child s to. + protected virtual void RenderChildren(ISvgRenderer renderer) { foreach (SvgElement element in this.Children) { @@ -655,10 +666,10 @@ namespace Svg } /// - /// Renders the and contents to the specified object. + /// Renders the and contents to the specified object. /// - /// The object to render to. - void ISvgElement.Render(SvgRenderer renderer) + /// The object to render to. + void ISvgElement.Render(ISvgRenderer renderer) { this.Render(renderer); } @@ -698,7 +709,7 @@ namespace Svg /// /// /// - protected GraphicsPath GetPaths(SvgElement elem, SvgRenderer renderer) + protected GraphicsPath GetPaths(SvgElement elem, ISvgRenderer renderer) { var ret = new GraphicsPath(); @@ -1117,6 +1128,6 @@ namespace Svg SvgElementCollection Children { get; } IList Nodes { get; } - void Render(SvgRenderer renderer); + void Render(ISvgRenderer renderer); } } \ No newline at end of file diff --git a/Source/SvgElementFactory.cs b/Source/SvgElementFactory.cs index 8f812bf..aaef9dd 100644 --- a/Source/SvgElementFactory.cs +++ b/Source/SvgElementFactory.cs @@ -15,7 +15,6 @@ namespace Svg internal class SvgElementFactory { private static List availableElements; - private const string svgNS = "http://www.w3.org/2000/svg"; private static Parser cssParser = new Parser(); /// @@ -84,7 +83,7 @@ namespace Svg //Trace.TraceInformation("Begin CreateElement: {0}", elementName); - if (elementNS == svgNS) + if (elementNS == SvgAttributeAttribute.SvgNamespace || string.IsNullOrEmpty(elementNS)) { if (elementName == "svg") { diff --git a/Source/SvgElementIdManager.cs b/Source/SvgElementIdManager.cs index a2d92b0..a4d10b7 100644 --- a/Source/SvgElementIdManager.cs +++ b/Source/SvgElementIdManager.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Net; +using System.IO; namespace Svg { @@ -39,6 +41,29 @@ namespace Svg public virtual SvgElement GetElementById(Uri uri) { + if (!uri.IsAbsoluteUri && this._document.BaseUri != null && !uri.ToString().StartsWith("#")) + { + var fullUri = new Uri(this._document.BaseUri, uri); + var hash = fullUri.OriginalString.Substring(fullUri.OriginalString.LastIndexOf('#')); + SvgDocument doc; + switch (fullUri.Scheme.ToLowerInvariant()) + { + case "file": + doc = SvgDocument.Open(fullUri.LocalPath.Substring(0, fullUri.LocalPath.Length - hash.Length)); + return doc.IdManager.GetElementById(hash); + case "http": + case "https": + var httpRequest = WebRequest.Create(uri); + using (WebResponse webResponse = httpRequest.GetResponse()) + { + doc = SvgDocument.Open(webResponse.GetResponseStream()); + return doc.IdManager.GetElementById(hash); + } + default: + throw new NotSupportedException(); + } + + } return this.GetElementById(uri.ToString()); } diff --git a/Source/SvgRenderer.cs b/Source/SvgRenderer.cs deleted file mode 100644 index 8ed934c..0000000 --- a/Source/SvgRenderer.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Text; - -namespace Svg -{ - /// - /// Convenience wrapper around a graphics object - /// - public sealed class SvgRenderer : IDisposable - { - private Graphics _innerGraphics; - private Stack _boundables = new Stack(); - - public void Boundable(ISvgBoundable boundable) - { - _boundables.Push(boundable); - } - public ISvgBoundable Boundable() - { - return _boundables.Peek(); - } - public ISvgBoundable PopBoundable() - { - return _boundables.Pop(); - } - - /// - /// Initializes a new instance of the class. - /// - private SvgRenderer() - { - } - - public Region Clip - { - get { return this._innerGraphics.Clip; } - set { this._innerGraphics.Clip = value; } - } - - /// - /// Creates a new from the specified . - /// - /// from which to create the new . - public static SvgRenderer FromImage(Image image) - { - SvgRenderer renderer = new SvgRenderer(); - renderer._innerGraphics = Graphics.FromImage(image); - return renderer; - } - - /// - /// Creates a new from the specified . - /// - /// The to create the renderer from. - public static SvgRenderer FromGraphics(Graphics graphics) - { - SvgRenderer renderer = new SvgRenderer(); - renderer._innerGraphics = graphics; - return renderer; - } - - public static SvgRenderer FromNull() - { - SvgRenderer renderer = new SvgRenderer(); - var img = new Bitmap(1, 1); - renderer._innerGraphics = Graphics.FromImage(img); - return renderer; - } - - public void DrawImageUnscaled(Image image, Point location) - { - this._innerGraphics.DrawImageUnscaled(image, location); - } - - public void DrawImage(Image image, RectangleF destRect, RectangleF srcRect, GraphicsUnit graphicsUnit) - { - _innerGraphics.DrawImage(image, destRect, srcRect, graphicsUnit); - } - - public void AddClip(Region region) - { - this._innerGraphics.SetClip(region, CombineMode.Intersect); - } - public void SetClip(Region region) - { - this._innerGraphics.SetClip(region, CombineMode.Replace); - } - - public void FillPath(Brush brush, GraphicsPath path) - { - this._innerGraphics.FillPath(brush, path); - } - - public void DrawPath(Pen pen, GraphicsPath path) - { - this._innerGraphics.DrawPath(pen, path); - } - - public void RotateTransform(float fAngle, MatrixOrder order) - { - this._innerGraphics.RotateTransform(fAngle, order); - } - - public void RotateTransform(float fAngle) - { - this.RotateTransform(fAngle, MatrixOrder.Append); - } - - public void TranslateTransform(float dx, float dy, MatrixOrder order) - { - this._innerGraphics.TranslateTransform(dx, dy, order); - } - - public void TranslateTransform(float dx, float dy) - { - this.TranslateTransform(dx, dy, MatrixOrder.Append); - } - - public void ScaleTransform(float sx, float sy, MatrixOrder order) - { - this._innerGraphics.ScaleTransform(sx, sy, order); - } - - public void ScaleTransform(float sx, float sy) - { - this.ScaleTransform(sx, sy, MatrixOrder.Append); - } - - public SmoothingMode SmoothingMode - { - get { return this._innerGraphics.SmoothingMode; } - set { this._innerGraphics.SmoothingMode = value; } - } - - public PixelOffsetMode PixelOffsetMode - { - get { return this._innerGraphics.PixelOffsetMode; } - set { this._innerGraphics.PixelOffsetMode = value; } - } - - public CompositingQuality CompositingQuality - { - get { return this._innerGraphics.CompositingQuality; } - set { this._innerGraphics.CompositingQuality = value; } - } - - public TextRenderingHint TextRenderingHint - { - get { return this._innerGraphics.TextRenderingHint; } - set { this._innerGraphics.TextRenderingHint = value; } - } - - public int TextContrast - { - get { return this._innerGraphics.TextContrast; } - set { this._innerGraphics.TextContrast = value; } - } - - public Matrix Transform - { - get { return this._innerGraphics.Transform; } - set { this._innerGraphics.Transform = value; } - } - - public void Save() - { - this._innerGraphics.Save(); - } - - public void Dispose() - { - this._innerGraphics.Dispose(); - } - - public SizeF MeasureString(string text, Font font) - { - var ff = font.FontFamily; - //Baseline calculation to match http://bobpowell.net/formattingtext.aspx - float ascent = ff.GetCellAscent(font.Style); - float baselineOffset = font.SizeInPoints / ff.GetEmHeight(font.Style) * ascent; - float baselineOffsetPixels = this._innerGraphics.DpiY / 72f * baselineOffset; - - StringFormat format = StringFormat.GenericTypographic; - format.SetMeasurableCharacterRanges(new CharacterRange[]{new CharacterRange(0, text.Length)}); - format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; - Region[] r = this._innerGraphics.MeasureCharacterRanges(text, font, new Rectangle(0, 0, 1000, 1000), format); - RectangleF rect = r[0].GetBounds(this._innerGraphics); - - return new SizeF(rect.Width, baselineOffsetPixels); - } - } -} \ No newline at end of file diff --git a/Source/SvgTextReader.cs b/Source/SvgTextReader.cs index 7ef556c..b04f48c 100644 --- a/Source/SvgTextReader.cs +++ b/Source/SvgTextReader.cs @@ -123,7 +123,6 @@ namespace Svg { const string entityText = " + /// http://stackoverflow.com/questions/3633000/net-enumerate-winforms-font-styles + /// + public class FontFamily + { + #region InstalledFont Parameters + + string _fontName = string.Empty; + string _fontSubFamily = string.Empty; + string _fontPath = string.Empty; + + #endregion + + #region InstalledFont Constructor + + private FontFamily(string fontName, string fontSubFamily, string fontPath) + { + _fontName = fontName; + _fontSubFamily = fontSubFamily; + _fontPath = fontPath; + } + + #endregion + + #region InstalledFont Properties + + public string FontName { get { return _fontName; } set { _fontName = value; } } + public string FontSubFamily { get { return _fontSubFamily; } set { _fontSubFamily = value; } } + public string FontPath { get { return _fontPath; } set { _fontPath = value; } } + + #endregion + + static public FontFamily FromPath(string fontFilePath) + { + string FontName = string.Empty; + string FontSubFamily = string.Empty; + string encStr = "UTF-8"; + string strRet = string.Empty; + + using (FileStream fs = new FileStream(fontFilePath, FileMode.Open, FileAccess.Read)) + { + + TT_OFFSET_TABLE ttOffsetTable = new TT_OFFSET_TABLE() + { + uMajorVersion = ReadUShort(fs), + uMinorVersion = ReadUShort(fs), + uNumOfTables = ReadUShort(fs), + uSearchRange = ReadUShort(fs), + uEntrySelector = ReadUShort(fs), + uRangeShift = ReadUShort(fs), + }; + + TT_TABLE_DIRECTORY tblDir = new TT_TABLE_DIRECTORY(); + bool found = false; + for (int i = 0; i <= ttOffsetTable.uNumOfTables; i++) + { + tblDir = new TT_TABLE_DIRECTORY(); + tblDir.Initialize(); + fs.Read(tblDir.szTag, 0, tblDir.szTag.Length); + tblDir.uCheckSum = ReadULong(fs); + tblDir.uOffset = ReadULong(fs); + tblDir.uLength = ReadULong(fs); + + Encoding enc = Encoding.GetEncoding(encStr); + string s = enc.GetString(tblDir.szTag); + + if (s.CompareTo("name") == 0) + { + found = true; + break; + } + } + + if (!found) return null; + + fs.Seek(tblDir.uOffset, SeekOrigin.Begin); + + TT_NAME_TABLE_HEADER ttNTHeader = new TT_NAME_TABLE_HEADER + { + uFSelector = ReadUShort(fs), + uNRCount = ReadUShort(fs), + uStorageOffset = ReadUShort(fs) + }; + + TT_NAME_RECORD ttRecord = new TT_NAME_RECORD(); + + for (int j = 0; j <= ttNTHeader.uNRCount; j++) + { + ttRecord = new TT_NAME_RECORD() + { + uPlatformID = ReadUShort(fs), + uEncodingID = ReadUShort(fs), + uLanguageID = ReadUShort(fs), + uNameID = ReadUShort(fs), + uStringLength = ReadUShort(fs), + uStringOffset = ReadUShort(fs) + }; + + if (ttRecord.uNameID > 2) { break; } + + long nPos = fs.Position; + fs.Seek(tblDir.uOffset + ttRecord.uStringOffset + ttNTHeader.uStorageOffset, SeekOrigin.Begin); + + byte[] buf = new byte[ttRecord.uStringLength]; + fs.Read(buf, 0, ttRecord.uStringLength); + + Encoding enc; + if (ttRecord.uEncodingID == 3 || ttRecord.uEncodingID == 1) + { + enc = Encoding.BigEndianUnicode; + } + else + { + enc = Encoding.UTF8; + } + + strRet = enc.GetString(buf); + if (ttRecord.uNameID == 1) { FontName = strRet; } + if (ttRecord.uNameID == 2) { FontSubFamily = strRet; } + + fs.Seek(nPos, SeekOrigin.Begin); + } + + return new FontFamily(FontName, FontSubFamily, fontFilePath); + } + } + + public struct TT_OFFSET_TABLE + { + public ushort uMajorVersion; + public ushort uMinorVersion; + public ushort uNumOfTables; + public ushort uSearchRange; + public ushort uEntrySelector; + public ushort uRangeShift; + } + + public struct TT_TABLE_DIRECTORY + { + public byte[] szTag; + public UInt32 uCheckSum; + public UInt32 uOffset; + public UInt32 uLength; + public void Initialize() + { + szTag = new byte[4]; + } + } + + public struct TT_NAME_TABLE_HEADER + { + public ushort uFSelector; + public ushort uNRCount; + public ushort uStorageOffset; + } + + public struct TT_NAME_RECORD + { + public ushort uPlatformID; + public ushort uEncodingID; + public ushort uLanguageID; + public ushort uNameID; + public ushort uStringLength; + public ushort uStringOffset; + } + + static private UInt16 ReadChar(FileStream fs, int characters) + { + string[] s = new string[characters]; + byte[] buf = new byte[Convert.ToByte(s.Length)]; + + buf = ReadAndSwap(fs, buf.Length); + return BitConverter.ToUInt16(buf, 0); + } + + static private UInt16 ReadByte(FileStream fs) + { + byte[] buf = new byte[11]; + buf = ReadAndSwap(fs, buf.Length); + return BitConverter.ToUInt16(buf, 0); + } + + static private UInt16 ReadUShort(FileStream fs) + { + byte[] buf = new byte[2]; + buf = ReadAndSwap(fs, buf.Length); + return BitConverter.ToUInt16(buf, 0); + } + + static private UInt32 ReadULong(FileStream fs) + { + byte[] buf = new byte[4]; + buf = ReadAndSwap(fs, buf.Length); + return BitConverter.ToUInt32(buf, 0); + } + + static private byte[] ReadAndSwap(FileStream fs, int size) + { + byte[] buf = new byte[size]; + fs.Read(buf, 0, buf.Length); + Array.Reverse(buf); + return buf; + } + } + + class Program + { + //static void Main(string[] args) + //{ + // System.Drawing.FontFamily fam; + // var allInstalledFonts = from e in Registry.LocalMachine.OpenSubKey("Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts", false).GetValueNames() + // select Registry.LocalMachine.OpenSubKey("Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts").GetValue(e); + + // var ttfFonts = from e in allInstalledFonts.Where(e => (e.ToString().EndsWith(".ttf") || e.ToString().EndsWith(".otf"))) select e; + // var ttfFontsPaths = from e in ttfFonts.Select(e => (Path.GetPathRoot(e.ToString()) == "") ? Environment.GetFolderPath(Environment.SpecialFolder.Fonts) + "\\" + e.ToString() : e.ToString()) select e; + // var fonts = from e in ttfFontsPaths.Select(e => GetFontDetails(e.ToString())) select e; + + // foreach (InstalledFont f in fonts) + // { + // if (f != null) + // Console.WriteLine("Name: " + f.FontName + ", SubFamily: " + f.FontSubFamily + ", Path: " + f.FontPath); + // } + + // Console.ReadLine(); + //} + + + } +} diff --git a/Source/Text/GdiFontDefn.cs b/Source/Text/GdiFontDefn.cs new file mode 100644 index 0000000..ef8d99d --- /dev/null +++ b/Source/Text/GdiFontDefn.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace Svg +{ + public class GdiFontDefn : IFontDefn + { + private Font _font; + + public float Size + { + get { return _font.Size; } + } + public float SizeInPoints + { + get { return _font.SizeInPoints; } + } + + public GdiFontDefn(Font font) + { + _font = font; + } + + public void AddStringToPath(ISvgRenderer renderer, GraphicsPath path, string text, PointF location) + { + path.AddString(text, _font.FontFamily, (int)_font.Style, _font.Size, location, StringFormat.GenericTypographic); + } + + //Baseline calculation to match http://bobpowell.net/formattingtext.aspx + public float Ascent(ISvgRenderer renderer) + { + var ff = _font.FontFamily; + float ascent = ff.GetCellAscent(_font.Style); + float baselineOffset = _font.SizeInPoints / ff.GetEmHeight(_font.Style) * ascent; + return renderer.DpiY / 72f * baselineOffset; + } + + public IList MeasureCharacters(ISvgRenderer renderer, string text) + { + var g = GetGraphics(renderer); + var regions = new List(); + StringFormat format; + for (int s = 0; s <= (text.Length - 1) / 32; s++) + { + format = StringFormat.GenericTypographic; + format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; + format.SetMeasurableCharacterRanges((from r in Enumerable.Range(32 * s, Math.Min(32, text.Length - 32 * s)) + select new CharacterRange(r, 1)).ToArray()); + regions.AddRange(from r in g.MeasureCharacterRanges(text, _font, new Rectangle(0, 0, 1000, 1000), format) + select r.GetBounds(g)); + } + return regions; + } + + public SizeF MeasureString(ISvgRenderer renderer, string text) + { + var g = GetGraphics(renderer); + StringFormat format = StringFormat.GenericTypographic; + format.SetMeasurableCharacterRanges(new CharacterRange[] { new CharacterRange(0, text.Length) }); + format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; + Region[] r = g.MeasureCharacterRanges(text, _font, new Rectangle(0, 0, 1000, 1000), format); + RectangleF rect = r[0].GetBounds(g); + + return new SizeF(rect.Width, Ascent(renderer)); + } + + private static Graphics _graphics; + private static Graphics GetGraphics(object renderer) + { + var provider = renderer as IGraphicsProvider; + if (provider == null) + { + if (_graphics == null) + { + var bmp = new Bitmap(1, 1); + _graphics = Graphics.FromImage(bmp); + } + return _graphics; + } + else + { + return provider.GetGraphics(); + } + } + } +} diff --git a/Source/Text/IFontDefn.cs b/Source/Text/IFontDefn.cs new file mode 100644 index 0000000..3b53896 --- /dev/null +++ b/Source/Text/IFontDefn.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace Svg +{ + public interface IFontDefn + { + float Size { get; } + float SizeInPoints { get; } + void AddStringToPath(ISvgRenderer renderer, GraphicsPath path, string text, PointF location); + float Ascent(ISvgRenderer renderer); + IList MeasureCharacters(ISvgRenderer renderer, string text); + SizeF MeasureString(ISvgRenderer renderer, string text); + } +} diff --git a/Source/Text/PathStatistics.cs b/Source/Text/PathStatistics.cs new file mode 100644 index 0000000..a02d968 --- /dev/null +++ b/Source/Text/PathStatistics.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing.Drawing2D; +using System.Drawing; + +namespace Svg +{ + public class PathStatistics + { + private const double GqBreak_TwoPoint = 0.57735026918962573; + private const double GqBreak_ThreePoint = 0.7745966692414834; + private const double GqBreak_FourPoint_01 = 0.33998104358485631; + private const double GqBreak_FourPoint_02 = 0.86113631159405257; + private const double GqWeight_FourPoint_01 = 0.65214515486254621; + private const double GqWeight_FourPoint_02 = 0.34785484513745385; + + private PathData _data; + private double _totalLength; + private List _segments = new List(); + + public double TotalLength { get { return _totalLength; } } + + public PathStatistics(PathData data) + { + _data = data; + int i = 1; + _totalLength = 0; + ISegment newSegment; + while (i < _data.Points.Length) + { + switch (_data.Types[i]) + { + case 1: + newSegment = new LineSegment(_data.Points[i - 1], _data.Points[i]); + i++; + break; + case 3: + newSegment = new CubicBezierSegment(_data.Points[i - 1], _data.Points[i], _data.Points[i + 1], _data.Points[i + 2]); + i+= 3; + break; + default: + throw new NotSupportedException(); + } + newSegment.StartOffset = _totalLength; + _segments.Add(newSegment); + _totalLength += newSegment.Length; + } + } + + + public void LocationAngleAtOffset(double offset, out PointF point, out float angle) + { + _segments[BinarySearchForSegment(offset, 0, _segments.Count - 1)].LocationAngleAtOffset(offset, out point, out angle); + } + public bool OffsetOnPath(double offset) + { + var seg = _segments[BinarySearchForSegment(offset, 0, _segments.Count - 1)]; + offset -= seg.StartOffset; + return (offset >= 0 && offset <= seg.Length); + } + + private int BinarySearchForSegment(double offset, int first, int last) + { + if (last == first) + { + return first; + } + else if ((last - first) == 1) + { + return (offset >= _segments[last].StartOffset ? last : first); + } + else + { + var mid = (last + first) / 2; + if (offset < _segments[mid].StartOffset) + { + return BinarySearchForSegment(offset, first, mid); + } + else + { + return BinarySearchForSegment(offset, mid, last); + } + } + } + + private interface ISegment + { + double StartOffset { get; set; } + double Length { get; } + void LocationAngleAtOffset(double offset, out PointF point, out float rotation); + } + + private class LineSegment : ISegment + { + private double _length; + private double _rotation; + private PointF _start; + private PointF _end; + + public double StartOffset { get; set; } + public double Length { get { return _length; } } + + public LineSegment(PointF start, PointF end) + { + _start = start; + _end = end; + _length = Math.Sqrt(Math.Pow(end.X - start.X, 2) + Math.Pow(end.Y - start.Y, 2)); + _rotation = Math.Atan2(end.Y - start.Y, end.X - start.X) * 180 / Math.PI; + } + + public void LocationAngleAtOffset(double offset, out PointF point, out float rotation) + { + offset -= StartOffset; + if (offset < 0 || offset > _length) throw new ArgumentOutOfRangeException(); + point = new PointF((float)(_start.X + (offset / _length) * (_end.X - _start.X)), + (float)(_start.Y + (offset / _length) * (_end.Y - _start.Y))); + rotation = (float)_rotation; + } + } + + private class CubicBezierSegment : ISegment + { + private PointF _p0; + private PointF _p1; + private PointF _p2; + private PointF _p3; + private double _length; + private Func _integral; + private SortedList _lengths = new SortedList(); + + public double StartOffset { get; set; } + public double Length { get { return _length; } } + + public CubicBezierSegment(PointF p0, PointF p1, PointF p2, PointF p3) + { + _p0 = p0; + _p1 = p1; + _p2 = p2; + _p3 = p3; + _integral = (t) => CubicBezierArcLengthIntegrand(_p0, _p1, _p2, _p3, t); + _length = GetLength(0, 1, 0.00000001f); + _lengths.Add(0, 0); + _lengths.Add(_length, 1); + } + + private double GetLength(double left, double right, double epsilon) + { + var fullInt = GaussianQuadrature(_integral, left, right, 4); + return Subdivide(left, right, fullInt, 0, epsilon); + } + private double Subdivide(double left, double right, double fullInt, double totalLength, double epsilon) + { + var mid = (left + right) / 2; + var leftValue = GaussianQuadrature(_integral, left, mid, 4); + var rightValue = GaussianQuadrature(_integral, mid, right, 4); + if (Math.Abs(fullInt - (leftValue + rightValue)) > epsilon) { + var leftSub = Subdivide(left, mid, leftValue, totalLength, epsilon / 2.0); + totalLength += leftSub; + AddElementToTable(mid, totalLength); + return Subdivide(mid, right, rightValue, totalLength, epsilon / 2.0) + leftSub; + } + else + { + return leftValue + rightValue; + } + } + private void AddElementToTable(double position, double totalLength) + { + _lengths.Add(totalLength, position); + } + + public void LocationAngleAtOffset(double offset, out PointF point, out float rotation) + { + offset -= StartOffset; + if (offset < 0 || offset > _length) throw new ArgumentOutOfRangeException(); + + var t = BinarySearchForParam(offset, 0, _lengths.Count - 1); + point = CubicBezierCurve(_p0, _p1, _p2, _p3, t); + var deriv = CubicBezierDerivative(_p0, _p1, _p2, _p3, t); + rotation = (float)(Math.Atan2(deriv.Y, deriv.X) * 180.0 / Math.PI); + } + private double BinarySearchForParam(double length, int first, int last) + { + if (last == first) + { + return _lengths.Values[last]; + } + else if ((last - first) == 1) + { + return _lengths.Values[first] + (_lengths.Values[last] - _lengths.Values[first]) * + (length - _lengths.Keys[first]) / (_lengths.Keys[last] - _lengths.Keys[first]); + } + else + { + var mid = (last + first) / 2; + if (length < _lengths.Keys[mid]) + { + return BinarySearchForParam(length, first, mid); + } + else + { + return BinarySearchForParam(length, mid, last); + } + } + } + + /// + /// Evaluates the integral of the function over the integral using the specified number of points + /// + /// + /// + /// + /// + public static double GaussianQuadrature(Func func, double a, double b, int points) + { + switch (points) + { + case 1: + return (b - a) * func.Invoke((a + b) / 2.0); + case 2: + return (b - a) / 2.0 * (func.Invoke((b - a) / 2.0 * -1 * GqBreak_TwoPoint + (a + b) / 2.0) + + func.Invoke((b - a) / 2.0 * GqBreak_TwoPoint + (a + b) / 2.0)); + case 3: + return (b - a) / 2.0 * (5.0 / 9 * func.Invoke((b - a) / 2.0 * -1 * GqBreak_ThreePoint + (a + b) / 2.0) + + 8.0 / 9 * func.Invoke((a + b) / 2.0) + + 5.0 / 9 * func.Invoke((b - a) / 2.0 * GqBreak_ThreePoint + (a + b) / 2.0)); + case 4: + return (b - a) / 2.0 * (GqWeight_FourPoint_01 * func.Invoke((b - a) / 2.0 * -1 * GqBreak_FourPoint_01 + (a + b) / 2.0) + + GqWeight_FourPoint_01 * func.Invoke((b - a) / 2.0 * GqBreak_FourPoint_01 + (a + b) / 2.0) + + GqWeight_FourPoint_02 * func.Invoke((b - a) / 2.0 * -1 * GqBreak_FourPoint_02 + (a + b) / 2.0) + + GqWeight_FourPoint_02 * func.Invoke((b - a) / 2.0 * GqBreak_FourPoint_02 + (a + b) / 2.0)); + } + throw new NotSupportedException(); + } + + /// http://en.wikipedia.org/wiki/B%C3%A9zier_curve + private PointF CubicBezierCurve(PointF p0, PointF p1, PointF p2, PointF p3, double t) + { + return new PointF((float)(Math.Pow(1 - t, 3) * p0.X + 3 * Math.Pow(1 - t, 2) * t * p1.X + + 3 * (1 - t) * Math.Pow(t, 2) * p2.X + Math.Pow(t, 3) * p3.X), + (float)(Math.Pow(1 - t, 3) * p0.Y + 3 * Math.Pow(1 - t, 2) * t * p1.Y + + 3 * (1 - t) * Math.Pow(t, 2) * p2.Y + Math.Pow(t, 3) * p3.Y)); + } + + /// http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html + private PointF CubicBezierDerivative(PointF p0, PointF p1, PointF p2, PointF p3, double t) + { + return new PointF((float)(3 * Math.Pow(1 - t, 2) * (p1.X - p0.X) + 6 * (1 - t) * t * (p2.X - p1.X) + 3 * Math.Pow(t, 2) * (p3.X - p2.X)), + (float)(3 * Math.Pow(1 - t, 2) * (p1.Y - p0.Y) + 6 * (1 - t) * t * (p2.Y - p1.Y) + 3 * Math.Pow(t, 2) * (p3.Y - p2.Y))); + } + + + private double CubicBezierArcLengthIntegrand(PointF p0, PointF p1, PointF p2, PointF p3, double t) + { + return Math.Sqrt(Math.Pow(3 * Math.Pow(1 - t, 2) * (p1.X - p0.X) + 6 * (1 - t) * t * (p2.X - p1.X) + 3 * Math.Pow(t, 2) * (p3.X - p2.X), 2) + + Math.Pow(3 * Math.Pow(1 - t, 2) * (p1.Y - p0.Y) + 6 * (1 - t) * t * (p2.Y - p1.Y) + 3 * Math.Pow(t, 2) * (p3.Y - p2.Y), 2)); + } + } + } +} diff --git a/Source/Text/SvgFont.cs b/Source/Text/SvgFont.cs new file mode 100644 index 0000000..da38bc7 --- /dev/null +++ b/Source/Text/SvgFont.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + [SvgElement("font")] + public class SvgFont : SvgElement + { + [SvgAttribute("horiz-adv-x")] + public float HorizAdvX + { + get { return (this.Attributes["horiz-adv-x"] == null ? 0 : (float)this.Attributes["horiz-adv-x"]); } + set { this.Attributes["horiz-adv-x"] = value; } + } + [SvgAttribute("horiz-origin-x")] + public float HorizOriginX + { + get { return (this.Attributes["horiz-origin-x"] == null ? 0 : (float)this.Attributes["horiz-origin-x"]); } + set { this.Attributes["horiz-origin-x"] = value; } + } + [SvgAttribute("horiz-origin-y")] + public float HorizOriginY + { + get { return (this.Attributes["horiz-origin-y"] == null ? 0 : (float)this.Attributes["horiz-origin-y"]); } + set { this.Attributes["horiz-origin-y"] = value; } + } + [SvgAttribute("vert-adv-y")] + public float VertAdvY + { + get { return (this.Attributes["vert-adv-y"] == null ? this.Children.OfType().First().UnitsPerEm : (float)this.Attributes["vert-adv-y"]); } + set { this.Attributes["vert-adv-y"] = value; } + } + [SvgAttribute("vert-origin-x")] + public float VertOriginX + { + get { return (this.Attributes["vert-origin-x"] == null ? this.HorizAdvX / 2 : (float)this.Attributes["vert-origin-x"]); } + set { this.Attributes["vert-origin-x"] = value; } + } + [SvgAttribute("vert-origin-y")] + public float VertOriginY + { + get { return (this.Attributes["vert-origin-y"] == null ? + (this.Children.OfType().First().Attributes["ascent"] == null ? 0 : this.Children.OfType().First().Ascent) : + (float)this.Attributes["vert-origin-y"]); } + set { this.Attributes["vert-origin-y"] = value; } + } + + public override SvgElement DeepCopy() + { + return base.DeepCopy(); + } + } +} diff --git a/Source/Text/SvgFontDefn.cs b/Source/Text/SvgFontDefn.cs new file mode 100644 index 0000000..d26f3b6 --- /dev/null +++ b/Source/Text/SvgFontDefn.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace Svg +{ + public class SvgFontDefn : IFontDefn + { + private SvgFont _font; + private float _emScale; + private float _ppi; + private float _size; + private Dictionary _glyphs; + private Dictionary _kerning; + + public float Size + { + get { return _size; } + } + + public float SizeInPoints + { + get { return _size * 72.0f / _ppi; } + } + + public SvgFontDefn (SvgFont font, float size, float ppi) + { + _font = font; + _size = size; + _ppi = ppi; + var face = _font.Children.OfType().First(); + _emScale = _size / face.UnitsPerEm; + } + + public float Ascent(ISvgRenderer renderer) + { + float ascent = _font.Descendants().OfType().First().Ascent; + float baselineOffset = this.SizeInPoints * (_emScale / _size) * ascent; + return renderer.DpiY / 72f * baselineOffset; + } + + public IList MeasureCharacters(ISvgRenderer renderer, string text) + { + var result = new List(); + GetPath(renderer, text, result, false); + return result; + } + + public System.Drawing.SizeF MeasureString(ISvgRenderer renderer, string text) + { + var result = new List(); + GetPath(renderer, text, result, true); + var nonEmpty = result.Where(r => r != RectangleF.Empty); + if (!nonEmpty.Any()) return SizeF.Empty; + return new SizeF(nonEmpty.Last().Right - nonEmpty.First().Left, Ascent(renderer)); + } + + public void AddStringToPath(ISvgRenderer renderer, GraphicsPath path, string text, PointF location) + { + var textPath = GetPath(renderer, text, null, false); + if (textPath.PointCount > 0) + { + var translate = new Matrix(); + translate.Translate(location.X, location.Y); + textPath.Transform(translate); + path.AddPath(textPath, false); + } + } + + private GraphicsPath GetPath(ISvgRenderer renderer, string text, IList ranges, bool measureSpaces) + { + EnsureDictionaries(); + + RectangleF bounds; + SvgGlyph glyph; + SvgKern kern; + GraphicsPath path; + SvgGlyph prevGlyph = null; + Matrix scaleMatrix; + float xPos = 0; + + var ascent = Ascent(renderer); + + var result = new GraphicsPath(); + if (string.IsNullOrEmpty(text)) return result; + + for (int i = 0; i < text.Length; i++) + { + if (!_glyphs.TryGetValue(text.Substring(i, 1), out glyph)) glyph = _font.Descendants().OfType().First(); + if (prevGlyph != null && _kerning.TryGetValue(prevGlyph.GlyphName + "|" + glyph.GlyphName, out kern)) + { + xPos -= kern.Kerning * _emScale; + } + path = (GraphicsPath)glyph.Path(renderer).Clone(); + scaleMatrix = new Matrix(); + scaleMatrix.Scale(_emScale, -1 * _emScale, MatrixOrder.Append); + scaleMatrix.Translate(xPos, ascent, MatrixOrder.Append); + path.Transform(scaleMatrix); + + bounds = path.GetBounds(); + if (ranges != null) + { + if (measureSpaces && bounds == RectangleF.Empty) + { + ranges.Add(new RectangleF(xPos, 0, glyph.HorizAdvX * _emScale, ascent)); + } + else + { + ranges.Add(bounds); + } + } + if (path.PointCount > 0) result.AddPath(path, false); + + xPos += glyph.HorizAdvX * _emScale; + prevGlyph = glyph; + } + + return result; + } + + private void EnsureDictionaries() + { + if (_glyphs == null) _glyphs = _font.Descendants().OfType().ToDictionary(g => g.Unicode ?? g.GlyphName ?? g.ID); + if (_kerning == null) _kerning = _font.Descendants().OfType().ToDictionary(k => k.Glyph1 + "|" + k.Glyph2); + } + } +} diff --git a/Source/Text/SvgFontFace.cs b/Source/Text/SvgFontFace.cs new file mode 100644 index 0000000..09aacae --- /dev/null +++ b/Source/Text/SvgFontFace.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + [SvgElement("font-face")] + public class SvgFontFace : SvgElement + { + [SvgAttribute("alphabetic")] + public float Alphabetic + { + get { return (this.Attributes["alphabetic"] == null ? 0 : (float)this.Attributes["alphabetic"]); } + set { this.Attributes["alphabetic"] = value; } + } + + [SvgAttribute("ascent")] + public float Ascent + { + get { return (this.Attributes["ascent"] == null ? this.UnitsPerEm - ((SvgFont)this.Parent).VertOriginY : (float)this.Attributes["ascent"]); } + set { this.Attributes["ascent"] = value; } + } + + [SvgAttribute("ascent-height")] + public float AscentHeight + { + get { return (this.Attributes["ascent-height"] == null ? this.Ascent : (float)this.Attributes["ascent-height"]); } + set { this.Attributes["ascent-height"] = value; } + } + + [SvgAttribute("descent")] + public float Descent + { + get { return (this.Attributes["descent"] == null ? ((SvgFont)this.Parent).VertOriginY : (float)this.Attributes["descent"]); } + set { this.Attributes["descent"] = value; } + } + + /// + /// Indicates which font family is to be used to render the text. + /// + [SvgAttribute("font-family")] + public virtual string FontFamily + { + get { return this.Attributes["font-family"] as string; } + set { this.Attributes["font-family"] = value; } + } + + /// + /// Refers to the size of the font from baseline to baseline when multiple lines of text are set solid in a multiline layout environment. + /// + [SvgAttribute("font-size")] + public virtual SvgUnit FontSize + { + get { return (this.Attributes["font-size"] == null) ? SvgUnit.Empty : (SvgUnit)this.Attributes["font-size"]; } + set { this.Attributes["font-size"] = value; } + } + + /// + /// Refers to the style of the font. + /// + [SvgAttribute("font-style")] + public virtual SvgFontStyle FontStyle + { + get { return (this.Attributes["font-style"] == null) ? SvgFontStyle.inherit : (SvgFontStyle)this.Attributes["font-style"]; } + set { this.Attributes["font-style"] = value; } + } + + /// + /// Refers to the varient of the font. + /// + [SvgAttribute("font-variant")] + public virtual SvgFontVariant FontVariant + { + get { return (this.Attributes["font-variant"] == null) ? SvgFontVariant.inherit : (SvgFontVariant)this.Attributes["font-variant"]; } + set { this.Attributes["font-variant"] = value; } + } + + /// + /// Refers to the boldness of the font. + /// + [SvgAttribute("font-weight")] + public virtual SvgFontWeight FontWeight + { + get { return (this.Attributes["font-weight"] == null) ? SvgFontWeight.inherit : (SvgFontWeight)this.Attributes["font-weight"]; } + set { this.Attributes["font-weight"] = value; } + } + + [SvgAttribute("panose-1")] + public string Panose1 + { + get { return this.Attributes["panose-1"] as string; } + set { this.Attributes["panose-1"] = value; } + } + + [SvgAttribute("units-per-em")] + public float UnitsPerEm + { + get { return (this.Attributes["units-per-em"] == null ? 1000 : (float)this.Attributes["units-per-em"]); } + set { this.Attributes["units-per-em"] = value; } + } + + [SvgAttribute("x-height")] + public float XHeight + { + get { return (this.Attributes["x-height"] == null ? float.MinValue : (float)this.Attributes["x-height"]); } + set { this.Attributes["x-height"] = value; } + } + + + public override SvgElement DeepCopy() + { + return base.DeepCopy(); + } + } +} diff --git a/Source/Text/SvgFontFaceSrc.cs b/Source/Text/SvgFontFaceSrc.cs new file mode 100644 index 0000000..5b0fdb6 --- /dev/null +++ b/Source/Text/SvgFontFaceSrc.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + [SvgElement("font-face-src")] + public class SvgFontFaceSrc : SvgElement + { + public override SvgElement DeepCopy() + { + return base.DeepCopy(); + } + } +} diff --git a/Source/Text/SvgFontFaceUri.cs b/Source/Text/SvgFontFaceUri.cs new file mode 100644 index 0000000..58d155d --- /dev/null +++ b/Source/Text/SvgFontFaceUri.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + [SvgElement("font-face-uri")] + public class SvgFontFaceUri : SvgElement + { + private Uri _referencedElement; + + [SvgAttribute("href", SvgAttributeAttribute.XLinkNamespace)] + public virtual Uri ReferencedElement + { + get { return this._referencedElement; } + set { this._referencedElement = value; } + } + + public override SvgElement DeepCopy() + { + return DeepCopy(); + } + + public override SvgElement DeepCopy() + { + var newObj = base.DeepCopy() as SvgFontFaceUri; + newObj.ReferencedElement = this.ReferencedElement; + + return newObj; + } + } +} diff --git a/Source/Text/SvgGlyph.cs b/Source/Text/SvgGlyph.cs new file mode 100644 index 0000000..bd1045b --- /dev/null +++ b/Source/Text/SvgGlyph.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Svg.Pathing; +using System.Drawing.Drawing2D; + +namespace Svg +{ + [SvgElement("glyph")] + public class SvgGlyph : SvgVisualElement + { + private GraphicsPath _path; + + /// + /// Gets or sets a of path data. + /// + [SvgAttribute("d")] + public SvgPathSegmentList PathData + { + get { return this.Attributes.GetAttribute("d"); } + set { this.Attributes["d"] = value; } + } + + [SvgAttribute("glyph-name")] + public virtual string GlyphName + { + get { return this.Attributes["glyph-name"] as string; } + set { this.Attributes["glyph-name"] = value; } + } + [SvgAttribute("horiz-adv-x")] + public float HorizAdvX + { + get { return (this.Attributes["horiz-adv-x"] == null ? this.Parents.OfType().First().HorizAdvX : (float)this.Attributes["horiz-adv-x"]); } + set { this.Attributes["horiz-adv-x"] = value; } + } + [SvgAttribute("unicode")] + public string Unicode + { + get { return this.Attributes["unicode"] as string; } + set { this.Attributes["unicode"] = value; } + } + [SvgAttribute("vert-adv-y")] + public float VertAdvY + { + get { return (this.Attributes["vert-adv-y"] == null ? this.Parents.OfType().First().VertAdvY : (float)this.Attributes["vert-adv-y"]); } + set { this.Attributes["vert-adv-y"] = value; } + } + [SvgAttribute("vert-origin-x")] + public float VertOriginX + { + get { return (this.Attributes["vert-origin-x"] == null ? this.Parents.OfType().First().VertOriginX : (float)this.Attributes["vert-origin-x"]); } + set { this.Attributes["vert-origin-x"] = value; } + } + [SvgAttribute("vert-origin-y")] + public float VertOriginY + { + get { return (this.Attributes["vert-origin-y"] == null ? this.Parents.OfType().First().VertOriginY : (float)this.Attributes["vert-origin-y"]); } + set { this.Attributes["vert-origin-y"] = value; } + } + + + /// + /// Gets the for this element. + /// + public override GraphicsPath Path(ISvgRenderer renderer) + { + if (this._path == null || this.IsPathDirty) + { + _path = new GraphicsPath(); + + foreach (SvgPathSegment segment in this.PathData) + { + segment.AddToPath(_path); + } + + this.IsPathDirty = false; + } + return _path; + } + + + /// + /// Gets or sets a value to determine if anti-aliasing should occur when the element is being rendered. + /// + protected override bool RequiresSmoothRendering + { + get { return true; } + } + + /// + /// Gets the bounds of the element. + /// + /// The bounds. + public override System.Drawing.RectangleF Bounds + { + get { return this.Path(null).GetBounds(); } + } + + /// + /// Initializes a new instance of the class. + /// + public SvgGlyph() + { + var pathData = new SvgPathSegmentList(); + this.Attributes["d"] = pathData; + } + + public override SvgElement DeepCopy() + { + return DeepCopy(); + } + + public override SvgElement DeepCopy() + { + var newObj = base.DeepCopy() as SvgGlyph; + foreach (var pathData in this.PathData) + newObj.PathData.Add(pathData.Clone()); + return newObj; + + } + } +} diff --git a/Source/Text/SvgKern.cs b/Source/Text/SvgKern.cs new file mode 100644 index 0000000..bdc5bd1 --- /dev/null +++ b/Source/Text/SvgKern.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + public abstract class SvgKern : SvgElement + { + [SvgAttribute("g1")] + public string Glyph1 + { + get { return this.Attributes["g1"] as string; } + set { this.Attributes["g1"] = value; } + } + [SvgAttribute("g2")] + public string Glyph2 + { + get { return this.Attributes["g2"] as string; } + set { this.Attributes["g2"] = value; } + } + [SvgAttribute("u1")] + public string Unicode1 + { + get { return this.Attributes["u1"] as string; } + set { this.Attributes["u1"] = value; } + } + [SvgAttribute("u2")] + public string Unicode2 + { + get { return this.Attributes["u2"] as string; } + set { this.Attributes["u2"] = value; } + } + [SvgAttribute("k")] + public float Kerning + { + get { return (this.Attributes["k"] == null ? 0 : (float)this.Attributes["k"]); } + set { this.Attributes["k"] = value; } + } + } + + [SvgElement("vkern")] + public class SvgVerticalKern : SvgKern + { + public override SvgElement DeepCopy() + { + return base.DeepCopy(); + } + } + [SvgElement("hkern")] + public class SvgHorizontalKern : SvgKern + { + public override SvgElement DeepCopy() + { + return base.DeepCopy(); + } + } +} diff --git a/Source/Text/SvgMissingGlyph.cs b/Source/Text/SvgMissingGlyph.cs new file mode 100644 index 0000000..22173ac --- /dev/null +++ b/Source/Text/SvgMissingGlyph.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + [SvgElement("missing-glyph")] + public class SvgMissingGlyph : SvgGlyph + { + [SvgAttribute("glyph-name")] + public override string GlyphName + { + get { return this.Attributes["glyph-name"] as string ?? "__MISSING_GLYPH__"; } + set { this.Attributes["glyph-name"] = value; } + } + } +} diff --git a/Source/Text/SvgTextBase.cs b/Source/Text/SvgTextBase.cs index 8729f8e..7e90db6 100644 --- a/Source/Text/SvgTextBase.cs +++ b/Source/Text/SvgTextBase.cs @@ -11,34 +11,15 @@ using System.Linq; namespace Svg { - public enum XmlSpaceHandling - { - @default, - preserve - } - public abstract class SvgTextBase : SvgVisualElement { - private SvgUnitCollection _x = new SvgUnitCollection(); - private SvgUnitCollection _y = new SvgUnitCollection(); - private SvgUnitCollection _dy = new SvgUnitCollection(); - private SvgUnitCollection _dx = new SvgUnitCollection(); - private SvgUnit _letterSpacing; - private SvgUnit _wordSpacing; - private static readonly SvgRenderer _stringMeasure; + protected SvgUnitCollection _x = new SvgUnitCollection(); + protected SvgUnitCollection _y = new SvgUnitCollection(); + protected SvgUnitCollection _dy = new SvgUnitCollection(); + protected SvgUnitCollection _dx = new SvgUnitCollection(); + private string _rotate; + private List _rotations = new List(); - private XmlSpaceHandling _space = XmlSpaceHandling.@default; - - /// - /// Initializes the class. - /// - static SvgTextBase() - { - Bitmap bitmap = new Bitmap(1, 1); - _stringMeasure = SvgRenderer.FromImage(bitmap); - _stringMeasure.TextRenderingHint = TextRenderingHint.AntiAlias; - } - /// /// Gets or sets the text to be rendered. /// @@ -66,6 +47,12 @@ namespace Svg set { this.Attributes["baseline-shift"] = value; this.IsPathDirty = true; } } + public override XmlSpaceHandling SpaceHandling + { + get { return base.SpaceHandling; } + set { base.SpaceHandling = value; this.IsPathDirty = true; } + } + /// /// Gets or sets the X. /// @@ -142,14 +129,56 @@ namespace Svg } } + /// + /// Gets or sets the rotate. + /// + /// The rotate. + [SvgAttribute("rotate")] + public virtual string Rotate + { + get { return this._rotate; } + set + { + if (_rotate != value) + { + this._rotate = value; + this._rotations.Clear(); + this._rotations.AddRange(from r in _rotate.Split(new char[] {',', ' ', '\r', '\n', '\t'}, StringSplitOptions.RemoveEmptyEntries) select float.Parse(r)); + this.IsPathDirty = true; + OnAttributeChanged(new AttributeEventArgs { Attribute = "rotate", Value = value }); + } + } + } + + /// + /// The pre-calculated length of the text + /// + [SvgAttribute("textLength")] + public virtual SvgUnit TextLength + { + get { return (this.Attributes["textLength"] == null ? SvgUnit.None : (SvgUnit)this.Attributes["textLength"]); } + set { this.Attributes["textLength"] = value; this.IsPathDirty = true; } + } + + /// + /// Gets or sets the text anchor. + /// + /// The text anchor. + [SvgAttribute("lengthAdjust")] + public virtual SvgTextLengthAdjust LengthAdjust + { + get { return (this.Attributes["lengthAdjust"] == null) ? SvgTextLengthAdjust.spacing : (SvgTextLengthAdjust)this.Attributes["lengthAdjust"]; } + set { this.Attributes["lengthAdjust"] = value; this.IsPathDirty = true; } + } + /// /// Specifies spacing behavior between text characters. /// [SvgAttribute("letter-spacing")] public virtual SvgUnit LetterSpacing { - get { return this._letterSpacing; } - set { this._letterSpacing = value; this.IsPathDirty = true; } + get { return (this.Attributes["letter-spacing"] == null ? SvgUnit.None : (SvgUnit)this.Attributes["letter-spacing"]); } + set { this.Attributes["letter-spacing"] = value; this.IsPathDirty = true; } } /// @@ -158,8 +187,8 @@ namespace Svg [SvgAttribute("word-spacing")] public virtual SvgUnit WordSpacing { - get { return this._wordSpacing; } - set { this._wordSpacing = value; this.IsPathDirty = true; } + get { return (this.Attributes["word-spacing"] == null ? SvgUnit.None : (SvgUnit)this.Attributes["word-spacing"]); } + set { this.Attributes["word-spacing"] = value; this.IsPathDirty = true; } } /// @@ -215,9 +244,9 @@ namespace Svg /// /// Renders the and contents to the specified object. /// - /// The object to render to. + /// The object to render to. /// Necessary to make sure that any internal tspan elements get rendered as well - protected override void Render(SvgRenderer renderer) + protected override void Render(ISvgRenderer renderer) { if ((this.Path(renderer) != null) && this.Visible && this.Displayable) { @@ -245,192 +274,135 @@ namespace Svg } } - private GraphicsPath _path; - - protected class NodeBounds + internal virtual IEnumerable GetContentNodes() { - public float xOffset { get; set; } - public SizeF Bounds { get; set; } - public ISvgNode Node { get; set; } + return (this.Nodes == null || this.Nodes.Count < 1 ? this.Children.OfType() : this.Nodes); } - protected class BoundsData + protected virtual GraphicsPath GetBaselinePath(ISvgRenderer renderer) { - private List _nodes = new List(); - public IList Nodes - { - get { return _nodes; } - } - public SizeF Bounds { get; set; } + return null; } - protected BoundsData GetTextBounds(SvgRenderer renderer) + protected virtual float GetAuthorPathLength() { - var font = GetFont(renderer); - SvgTextBase innerText; - SizeF stringBounds; - float totalHeight = 0; - float totalWidth = 0; - - var result = new BoundsData(); - var nodes = (from n in this.Nodes - where (n is SvgContentNode || n is SvgTextBase) && !string.IsNullOrEmpty(n.Content) - select n).ToList(); - - // Individual character spacing - if (nodes.FirstOrDefault() is SvgContentNode && _x.Count > 1) - { - string ch; - var content = nodes.First() as SvgContentNode; - nodes.RemoveAt(0); - int posCount = Math.Min(content.Content.Length, _x.Count); - var text = PrepareText(content.Content, false, (nodes.Count > 1 && nodes[1] is SvgTextBase)); - - for (var i = 0; i < posCount; i++) - { - ch = (i == posCount - 1 ? text.Substring(i) : text.Substring(i, 1)); - stringBounds = _stringMeasure.MeasureString(ch, font); - totalHeight = Math.Max(totalHeight, stringBounds.Height); - result.Nodes.Add(new NodeBounds() - { - Bounds = stringBounds, - Node = new SvgContentNode() { Content = ch }, - xOffset = (i == 0 ? 0 : _x[i].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this) - - _x[0].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this)) - }); - } - } - - // Calculate the bounds of the text - ISvgNode node; - var accumulateDims = true; - for (var i = 0; i < nodes.Count; i++) - { - node = nodes[i]; - lock (_stringMeasure) - { - innerText = node as SvgTextBase; - if (innerText == null) - { - stringBounds = _stringMeasure.MeasureString(PrepareText(node.Content, - i > 0 && nodes[i - 1] is SvgTextBase, - i < nodes.Count - 1 && nodes[i + 1] is SvgTextBase), font); - result.Nodes.Add(new NodeBounds() { Bounds = stringBounds, Node = node, xOffset = totalWidth }); - } - else - { - stringBounds = innerText.GetTextBounds(renderer).Bounds; - result.Nodes.Add(new NodeBounds() { Bounds = stringBounds, Node = node, xOffset = totalWidth }); - accumulateDims = accumulateDims && SvgUnitCollection.IsNullOrEmpty(innerText.X) && SvgUnitCollection.IsNullOrEmpty(innerText.Y); - if (accumulateDims && innerText.Dx.Count == 1) totalWidth += innerText.Dx[0].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this); - } - - if (accumulateDims) - { - totalHeight = Math.Max(totalHeight, stringBounds.Height); - totalWidth += stringBounds.Width; - } - } - } - result.Bounds = new SizeF(totalWidth, totalHeight); - return result; + return 0; } - protected float _calcX = 0; - protected float _calcY = 0; + private GraphicsPath _path; /// /// Gets the for this element. /// /// - public override System.Drawing.Drawing2D.GraphicsPath Path(SvgRenderer renderer) + public override System.Drawing.Drawing2D.GraphicsPath Path(ISvgRenderer renderer) { // Make sure the path is always null if there is no text //if there is a TSpan inside of this text element then path should not be null (even if this text is empty!) - if ((string.IsNullOrEmpty(this.Text) || this.Text.Trim().Length < 1) && this.Children.Where(x => x is SvgTextSpan).Select(x => x as SvgTextSpan).Count() == 0) - return _path = null; - //NOT SURE WHAT THIS IS ABOUT - Path gets created again anyway - WTF? - // When an empty string is passed to GraphicsPath, it rises an InvalidArgumentException... - + var nodes = this.GetContentNodes().ToList(); + if (nodes.Count < 1) return _path = null; + if (nodes.Count == 1 && nodes[0] is SvgContentNode && + (string.IsNullOrEmpty(nodes[0].Content) || nodes[0].Content.Trim().Length < 1)) return _path = null; + if (_path == null || this.IsPathDirty) { renderer = (renderer ?? SvgRenderer.FromNull()); - // Measure the overall bounds of all the text - var boundsData = GetTextBounds(renderer); - - var font = GetFont(renderer); - SvgTextBase innerText; - float x = (_x.Count < 1 ? _calcX : _x[0].ToDeviceValue(renderer, UnitRenderingType.HorizontalOffset, this)) + - (_dx.Count < 1 ? 0 : _dx[0].ToDeviceValue(renderer, UnitRenderingType.Horizontal, this)); - float y = (_y.Count < 1 ? _calcY : _y[0].ToDeviceValue(renderer, UnitRenderingType.VerticalOffset, this)) + - (_dy.Count < 1 ? 0 : _dy[0].ToDeviceValue(renderer, UnitRenderingType.Vertical, this)); + this.SetPath(new TextDrawingState(renderer, this)); + } + return _path; + } - _path = new GraphicsPath(); - _path.StartFigure(); + private void SetPath(TextDrawingState state) + { + SetPath(state, true); + } - // Determine the location of the start point - switch (this.TextAnchor) + /// + /// Sets the path on this element and all child elements. Uses the state + /// object to track the state of the drawing + /// + /// State of the drawing operation + private void SetPath(TextDrawingState state, bool doMeasurements) + { + SvgTextBase inner; + TextDrawingState newState; + TextDrawingState origState = null; + bool alignOnBaseline = state.BaselinePath != null && (this.TextAnchor == SvgTextAnchor.Middle || this.TextAnchor == SvgTextAnchor.End); + if (doMeasurements) + { + if (this.TextLength != SvgUnit.None) + { + origState = state.Clone(); + } + else if (alignOnBaseline) { - case SvgTextAnchor.Middle: - x -= (boundsData.Bounds.Width / 2); - break; - case SvgTextAnchor.End: - x -= boundsData.Bounds.Width; - break; + origState = state.Clone(); + state.BaselinePath = null; } + } - try + foreach (var node in GetContentNodes()) + { + inner = node as SvgTextBase; + if (inner == null) { - renderer.Boundable(new FontBoundable(font)); - switch (this.BaselineShift) - { - case null: - case "": - case "baseline": - case "inherit": - // do nothing - break; - case "sub": - y += new SvgUnit(SvgUnitType.Ex, 1).ToDeviceValue(renderer, UnitRenderingType.Vertical, this); - break; - case "super": - y -= new SvgUnit(SvgUnitType.Ex, 1).ToDeviceValue(renderer, UnitRenderingType.Vertical, this); - break; - default: - var convert = new SvgUnitConverter(); - var shift = (SvgUnit)convert.ConvertFromInvariantString(this.BaselineShift); - y -= shift.ToDeviceValue(renderer, UnitRenderingType.Vertical, this); - break; - } + if (!string.IsNullOrEmpty(node.Content)) state.DrawString(PrepareText(node.Content)); } - finally + else { - renderer.PopBoundable(); + newState = new TextDrawingState(state, inner); + inner.SetPath(newState); + state.NumChars += newState.NumChars; + state.Current = newState.Current; } + } + + var path = state.GetPath() ?? new GraphicsPath(); - NodeBounds data; - var yCummOffset = 0.0f; - for (var i = 0; i < boundsData.Nodes.Count; i++) + // Apply any text length adjustments + if (doMeasurements) + { + if (this.TextLength != SvgUnit.None) + { + var bounds = path.GetBounds(); + var specLength = this.TextLength.ToDeviceValue(state.Renderer, UnitRenderingType.Horizontal, this); + var actLength = bounds.Width; + var diff = (actLength - specLength); + if (Math.Abs(diff) > 1.5) + { + if (this.LengthAdjust == SvgTextLengthAdjust.spacing) + { + origState.LetterSpacingAdjust = -1 * diff / (state.NumChars - origState.NumChars - 1); + SetPath(origState, false); + return; + } + else + { + var matrix = new Matrix(); + matrix.Translate(-1 * bounds.X, 0, MatrixOrder.Append); + matrix.Scale(specLength / actLength, 1, MatrixOrder.Append); + matrix.Translate(bounds.X, 0, MatrixOrder.Append); + path.Transform(matrix); + } + } + } + else if (alignOnBaseline) { - data = boundsData.Nodes[i]; - innerText = data.Node as SvgTextBase; - if (innerText == null) + var bounds = path.GetBounds(); + if (this.TextAnchor == SvgTextAnchor.Middle) { - // Minus FontSize because the x/y coords mark the bottom left, not bottom top. - DrawString(renderer, _path, x + data.xOffset, y - boundsData.Bounds.Height, font, - PrepareText(data.Node.Content, i > 0 && boundsData.Nodes[i - 1].Node is SvgTextBase, - i < boundsData.Nodes.Count - 1 && boundsData.Nodes[i + 1].Node is SvgTextBase)); + origState.StartOffsetAdjust = -1 * bounds.Width / 2; } else { - innerText._calcX = x + data.xOffset; - innerText._calcY = y + yCummOffset; - if (innerText.Dy.Count == 1) yCummOffset += innerText.Dy[0].ToDeviceValue(renderer, UnitRenderingType.Vertical, this); + origState.StartOffsetAdjust = -1 * bounds.Width; } + SetPath(origState, false); + return; } - - _path.CloseFigure(); - this.IsPathDirty = false; } - return _path; + + + _path = path; + this.IsPathDirty = false; } private static readonly Regex MultipleSpaces = new Regex(@" {2,}", RegexOptions.Compiled); @@ -440,67 +412,19 @@ namespace Svg /// /// Text to be prepared /// Prepared text - protected string PrepareText(string value, bool leadingSpace, bool trailingSpace) + protected string PrepareText(string value) { - if (_space == XmlSpaceHandling.preserve) + if (this.SpaceHandling == XmlSpaceHandling.preserve) { return value.Replace('\t', ' ').Replace("\r\n", " ").Replace('\r', ' ').Replace('\n', ' '); } else { var convValue = MultipleSpaces.Replace(value.Replace("\r", "").Replace("\n", "").Replace('\t', ' '), " "); - //if (!leadingSpace) convValue = convValue.TrimStart(); - //if (!trailingSpace) convValue = convValue.TrimEnd(); return convValue; } } - /// - /// Draws a string on a path at a specified location and with a specified font. - /// - internal void DrawString(SvgRenderer renderer, GraphicsPath path, float x, float y, Font font, string text) - { - PointF location = new PointF(x, y); - - // No way to do letter-spacing or word-spacing, so do manually - if (this.LetterSpacing.Value > 0.0f || this.WordSpacing.Value > 0.0f) - { - // Cut up into words, or just leave as required - string[] words = (this.WordSpacing.Value > 0.0f) ? text.Split(' ') : new string[] { text }; - float wordSpacing = this.WordSpacing.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this); - float letterSpacing = this.LetterSpacing.ToDeviceValue(renderer, UnitRenderingType.Horizontal, this); - float start = x; - - foreach (string word in words) - { - // Only do if there is line spacing, just write the word otherwise - if (this.LetterSpacing.Value > 0.0f) - { - char[] characters = word.ToCharArray(); - foreach (char currentCharacter in characters) - { - path.AddString(currentCharacter.ToString(), font.FontFamily, (int)font.Style, font.Size, location, StringFormat.GenericTypographic); - location = new PointF(path.GetBounds().Width + start + letterSpacing, location.Y); - } - } - else - { - path.AddString(word, font.FontFamily, (int)font.Style, font.Size, location, StringFormat.GenericTypographic); - } - - // Move the location of the word to be written along - location = new PointF(path.GetBounds().Width + start + wordSpacing, location.Y); - } - } - else - { - if (!string.IsNullOrEmpty(text)) - { - path.AddString(text, font.FontFamily, (int)font.Style, font.Size, location, StringFormat.GenericTypographic); - } - } - } - [SvgAttribute("onchange")] public event EventHandler Change; @@ -519,6 +443,23 @@ namespace Svg } } + + + //private static GraphicsPath GetPath(string text, Font font) + //{ + // var fontMetrics = (from c in text.Distinct() + // select new { Char = c, Metrics = Metrics(c, font) }). + // ToDictionary(c => c.Char, c=> c.Metrics); + // // Measure each character and check the metrics against the overall metrics of rendering + // // an entire word with kerning. + //} + //private static RectangleF Metrics(char c, Font font) + //{ + // var path = new GraphicsPath(); + // path.AddString(c.ToString(), font.FontFamily, (int)font.Style, font.Size, new Point(0, 0), StringFormat.GenericTypographic); + // return path.GetBounds(); + //} + #if Net4 public override void RegisterEvents(ISvgEventCaller caller) { @@ -542,11 +483,17 @@ namespace Svg private class FontBoundable : ISvgBoundable { - private Font _font; + private IFontDefn _font; + private float _width = 1; - public FontBoundable(Font font) + public FontBoundable(IFontDefn font) + { + _font = font; + } + public FontBoundable(IFontDefn font, float width) { _font = font; + _width = width; } public PointF Location @@ -556,7 +503,7 @@ namespace Svg public SizeF Size { - get { return new SizeF(1, _font.Size); } + get { return new SizeF(_width, _font.Size); } } public RectangleF Bounds @@ -564,5 +511,402 @@ namespace Svg get { return new RectangleF(this.Location, this.Size); } } } + + private class TextDrawingState + { + private float _xAnchor = float.MinValue; + private IList _anchoredPaths = new List(); + private GraphicsPath _currPath = null; + private GraphicsPath _finalPath = null; + private float _authorPathLength = 0; + + public GraphicsPath BaselinePath { get; set; } + public PointF Current { get; set; } + public SvgTextBase Element { get; set; } + public float LetterSpacingAdjust { get; set; } + public int NumChars { get; set; } + public TextDrawingState Parent { get; set; } + public ISvgRenderer Renderer { get; set; } + public float StartOffsetAdjust { get; set; } + + private TextDrawingState() { } + public TextDrawingState(ISvgRenderer renderer, SvgTextBase element) + { + this.Element = element; + this.Renderer = renderer; + this.Current = PointF.Empty; + _xAnchor = 0; + this.BaselinePath = element.GetBaselinePath(renderer); + _authorPathLength = element.GetAuthorPathLength(); + } + public TextDrawingState(TextDrawingState parent, SvgTextBase element) + { + this.Element = element; + this.Renderer = parent.Renderer; + this.Parent = parent; + this.Current = parent.Current; + this.BaselinePath = element.GetBaselinePath(parent.Renderer) ?? parent.BaselinePath; + var currPathLength = element.GetAuthorPathLength(); + _authorPathLength = currPathLength == 0 ? parent._authorPathLength : currPathLength; + } + + public GraphicsPath GetPath() + { + FlushPath(); + return _finalPath; + } + + public TextDrawingState Clone() + { + var result = new TextDrawingState(); + result._anchoredPaths = this._anchoredPaths.ToList(); + result.BaselinePath = this.BaselinePath; + result._xAnchor = this._xAnchor; + result.Current = this.Current; + result.Element = this.Element; + result.NumChars = this.NumChars; + result.Parent = this.Parent; + result.Renderer = this.Renderer; + return result; + } + + public void DrawString(string value) + { + // Get any defined anchors + var xAnchors = GetValues(value.Length, e => e._x, UnitRenderingType.HorizontalOffset); + var yAnchors = GetValues(value.Length, e => e._y, UnitRenderingType.VerticalOffset); + var font = this.Element.GetFont(this.Renderer); + var fontBaselineHeight = this.Renderer.FontBaselineOffset(font); + PathStatistics pathStats = null; + var pathScale = 1.0; + if (BaselinePath != null) + { + pathStats = new PathStatistics(BaselinePath.PathData); + if (_authorPathLength > 0) pathScale = _authorPathLength / pathStats.TotalLength; + } + + // Get all of the offsets (explicit and defined by spacing) + IList xOffsets; + IList yOffsets; + IList rotations; + float baselineShift = 0.0f; + + try + { + this.Renderer.SetBoundable(new FontBoundable(font, (float)(pathStats == null ? 1 : pathStats.TotalLength))); + xOffsets = GetValues(value.Length, e => e._dx, UnitRenderingType.Horizontal); + yOffsets = GetValues(value.Length, e => e._dy, UnitRenderingType.Vertical); + if (StartOffsetAdjust != 0.0f) + { + if (xOffsets.Count < 1) + { + xOffsets.Add(StartOffsetAdjust); + } + else + { + xOffsets[0] += StartOffsetAdjust; + } + } + + if (this.Element.LetterSpacing.Value != 0.0f || this.Element.WordSpacing.Value != 0.0f || this.LetterSpacingAdjust != 0.0f) + { + var spacing = this.Element.LetterSpacing.ToDeviceValue(this.Renderer, UnitRenderingType.Horizontal, this.Element) + this.LetterSpacingAdjust; + var wordSpacing = this.Element.WordSpacing.ToDeviceValue(this.Renderer, UnitRenderingType.Horizontal, this.Element); + if (this.Parent == null && this.NumChars == 0 && xOffsets.Count < 1) xOffsets.Add(0); + for (int i = (this.Parent == null && this.NumChars == 0 ? 1 : 0); i < value.Length; i++) + { + if (i >= xOffsets.Count) + { + xOffsets.Add(spacing + (char.IsWhiteSpace(value[i]) ? wordSpacing : 0)); + } + else + { + xOffsets[i] += spacing + (char.IsWhiteSpace(value[i]) ? wordSpacing : 0); + } + } + } + + rotations = GetValues(value.Length, e => e._rotations); + + // Calculate Y-offset due to baseline shift. Don't inherit the value so that it is not accumulated multiple times. + var baselineShiftText = this.Element.Attributes.GetAttribute("baseline-shift"); + + switch (baselineShiftText) + { + case null: + case "": + case "baseline": + case "inherit": + // do nothing + break; + case "sub": + baselineShift = new SvgUnit(SvgUnitType.Ex, 1).ToDeviceValue(this.Renderer, UnitRenderingType.Vertical, this.Element); + break; + case "super": + baselineShift = -1 * new SvgUnit(SvgUnitType.Ex, 1).ToDeviceValue(this.Renderer, UnitRenderingType.Vertical, this.Element); + break; + default: + var convert = new SvgUnitConverter(); + var shiftUnit = (SvgUnit)convert.ConvertFromInvariantString(baselineShiftText); + baselineShift = -1 * shiftUnit.ToDeviceValue(this.Renderer, UnitRenderingType.Vertical, this.Element); + break; + } + + if (baselineShift != 0.0f) + { + if (yOffsets.Any()) + { + yOffsets[0] += baselineShift; + } + else + { + yOffsets.Add(baselineShift); + } + } + } + finally + { + this.Renderer.PopBoundable(); + } + + // NOTE: Assuming a horizontal left-to-right font + // Render absolutely positioned items in the horizontal direction + var yPos = Current.Y; + for (int i = 0; i < xAnchors.Count - 1; i++) + { + FlushPath(); + _xAnchor = xAnchors[i] + (xOffsets.Count > i ? xOffsets[i] : 0); + EnsurePath(); + yPos = (yAnchors.Count > i ? yAnchors[i] : yPos) + (yOffsets.Count > i ? yOffsets[i] : 0); + + DrawStringOnCurrPath(value[i].ToString(), font, new PointF(_xAnchor, yPos), + fontBaselineHeight, (rotations.Count > i ? rotations[i] : rotations.LastOrDefault())); + } + + // Render any remaining characters + var renderChar = 0; + var xPos = this.Current.X; + if (xAnchors.Any()) + { + FlushPath(); + renderChar = xAnchors.Count - 1; + xPos = xAnchors.Last(); + _xAnchor = xPos; + } + EnsurePath(); + + + // Render individual characters as necessary + var lastIndividualChar = renderChar + Math.Max(Math.Max(Math.Max(Math.Max(xOffsets.Count, yOffsets.Count), yAnchors.Count), rotations.Count) - renderChar - 1, 0); + if (rotations.LastOrDefault() != 0.0f || pathStats != null) lastIndividualChar = value.Length; + if (lastIndividualChar > renderChar) + { + var charBounds = this.Renderer.MeasureCharacters(value.Substring(renderChar, Math.Min(lastIndividualChar + 1, value.Length) - renderChar), font); + PointF pathPoint; + float rotation; + float halfWidth; + for (int i = renderChar; i < lastIndividualChar; i++) + { + xPos += (float)pathScale * (xOffsets.Count > i ? xOffsets[i] : 0) + (charBounds[i - renderChar].X - (i == renderChar ? 0 : charBounds[i - renderChar - 1].X)); + yPos = (yAnchors.Count > i ? yAnchors[i] : yPos) + (yOffsets.Count > i ? yOffsets[i] : 0); + if (pathStats == null) + { + DrawStringOnCurrPath(value[i].ToString(), font, new PointF(xPos, yPos), + fontBaselineHeight, (rotations.Count > i ? rotations[i] : rotations.LastOrDefault())); + } + else + { + xPos = Math.Max(xPos, 0); + halfWidth = charBounds[i-renderChar].Width / 2; + if (pathStats.OffsetOnPath(xPos + halfWidth)) + { + pathStats.LocationAngleAtOffset(xPos + halfWidth, out pathPoint, out rotation); + pathPoint = new PointF((float)(pathPoint.X - halfWidth * Math.Cos(rotation * Math.PI / 180) - (float)pathScale * yPos * Math.Sin(rotation * Math.PI / 180)), + (float)(pathPoint.Y - halfWidth * Math.Sin(rotation * Math.PI / 180) + (float)pathScale * yPos * Math.Cos(rotation * Math.PI / 180))); + DrawStringOnCurrPath(value[i].ToString(), font, pathPoint, fontBaselineHeight, rotation); + } + } + } + + // Add the kerning to the next character + if (lastIndividualChar < value.Length) + { + xPos += charBounds[charBounds.Count - 1].X - charBounds[charBounds.Count - 2].X; + } + else + { + xPos += charBounds.Last().Width; + } + } + + // Render the string normally + if (lastIndividualChar < value.Length) + { + xPos += (xOffsets.Count > lastIndividualChar ? xOffsets[lastIndividualChar] : 0); + yPos = (yAnchors.Count > lastIndividualChar ? yAnchors[lastIndividualChar] : yPos) + + (yOffsets.Count > lastIndividualChar ? yOffsets[lastIndividualChar] : 0); + DrawStringOnCurrPath(value.Substring(lastIndividualChar), font, new PointF(xPos, yPos), + fontBaselineHeight, rotations.LastOrDefault()); + var bounds = this.Renderer.MeasureString(value.Substring(lastIndividualChar), font); + xPos += bounds.Width; + } + + + NumChars += value.Length; + // Undo any baseline shift. This is not persisted, unlike normal vertical offsets. + this.Current = new PointF(xPos, yPos - baselineShift); + } + + private void DrawStringOnCurrPath(string value, IFontDefn font, PointF location, float fontBaselineHeight, float rotation) + { + var drawPath = _currPath; + if (rotation != 0.0f) drawPath = new GraphicsPath(); + font.AddStringToPath(this.Renderer, drawPath, value, new PointF(location.X, location.Y - fontBaselineHeight)); + if (rotation != 0.0f && drawPath.PointCount > 0) + { + var matrix = new Matrix(); + matrix.Translate(-1 * location.X, -1 * location.Y, MatrixOrder.Append); + matrix.Rotate(rotation, MatrixOrder.Append); + matrix.Translate(location.X, location.Y, MatrixOrder.Append); + drawPath.Transform(matrix); + _currPath.AddPath(drawPath, false); + } + + } + + private void EnsurePath() + { + if (_currPath == null) + { + _currPath = new GraphicsPath(); + _currPath.StartFigure(); + + var currState = this; + while (currState != null && currState._xAnchor <= float.MinValue) + { + currState = currState.Parent; + } + currState._anchoredPaths.Add(_currPath); + } + } + + private void FlushPath() + { + if (_currPath != null) + { + _currPath.CloseFigure(); + + // Abort on empty paths (e.g. rendering a space) + if (_currPath.PointCount < 1) + { + _anchoredPaths.Clear(); + _xAnchor = float.MinValue; + _currPath = null; + return; + } + + if (_xAnchor > float.MinValue) + { + float minX = float.MaxValue; + float maxX = float.MinValue; + RectangleF bounds; + foreach (var path in _anchoredPaths) + { + bounds = path.GetBounds(); + if (bounds.Left < minX) minX = bounds.Left; + if (bounds.Right > maxX) maxX = bounds.Right; + } + + var xOffset = _xAnchor - minX; + switch (Element.TextAnchor) + { + case SvgTextAnchor.Middle: + xOffset -= (maxX - minX) / 2; + break; + case SvgTextAnchor.End: + xOffset -= (maxX - minX); + break; + } + + if (xOffset != 0) + { + var matrix = new Matrix(); + matrix.Translate(xOffset, 0); + foreach (var path in _anchoredPaths) + { + path.Transform(matrix); + } + } + + _anchoredPaths.Clear(); + _xAnchor = float.MinValue; + + } + + if (_finalPath == null) + { + _finalPath = _currPath; + } + else + { + _finalPath.AddPath(_currPath, false); + } + + _currPath = null; + } + } + + private IList GetValues(int maxCount, Func> listGetter) + { + var currState = this; + int charCount = 0; + var results = new List(); + int resultCount = 0; + + while (currState != null) + { + charCount += currState.NumChars; + results.AddRange(listGetter.Invoke(currState.Element).Skip(charCount).Take(maxCount)); + if (results.Count > resultCount) + { + maxCount -= results.Count - resultCount; + charCount += results.Count - resultCount; + resultCount = results.Count; + } + + if (maxCount < 1) return results; + + currState = currState.Parent; + } + + return results; + } + private IList GetValues(int maxCount, Func> listGetter, UnitRenderingType renderingType) + { + var currState = this; + int charCount = 0; + var results = new List(); + int resultCount = 0; + + while (currState != null) + { + charCount += currState.NumChars; + results.AddRange(listGetter.Invoke(currState.Element).Skip(charCount).Take(maxCount).Select(p => p.ToDeviceValue(currState.Renderer, renderingType, currState.Element))); + if (results.Count > resultCount) + { + maxCount -= results.Count - resultCount; + charCount += results.Count - resultCount; + resultCount = results.Count; + } + + if (maxCount < 1) return results; + + currState = currState.Parent; + } + + return results; + } + } + } } diff --git a/Source/Text/SvgTextPath.cs b/Source/Text/SvgTextPath.cs new file mode 100644 index 0000000..e80b866 --- /dev/null +++ b/Source/Text/SvgTextPath.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Diagnostics; + +namespace Svg +{ + /// + /// The element defines a graphics element consisting of text. + /// + [SvgElement("textPath")] + public class SvgTextPath : SvgTextBase + { + private Uri _referencedPath; + + public override SvgUnitCollection Dx + { + get { return null; } + set { /* do nothing */ } + } + + [SvgAttribute("startOffset")] + public virtual SvgUnit StartOffset + { + get { return (_dx.Count < 1 ? SvgUnit.None : _dx[0]); } + set + { + if (_dx.Count < 1) + { + _dx.Add(value); + } + else + { + _dx[0] = value; + } + } + } + + [SvgAttribute("method")] + public virtual SvgTextPathMethod Method + { + get { return (this.Attributes["method"] == null ? SvgTextPathMethod.align : (SvgTextPathMethod)this.Attributes["method"]); } + set { this.Attributes["method"] = value; } + } + + [SvgAttribute("spacing")] + public virtual SvgTextPathSpacing Spacing + { + get { return (this.Attributes["spacing"] == null ? SvgTextPathSpacing.exact : (SvgTextPathSpacing)this.Attributes["spacing"]); } + set { this.Attributes["spacing"] = value; } + } + + [SvgAttribute("href", SvgAttributeAttribute.XLinkNamespace)] + public virtual Uri ReferencedPath + { + get { return this._referencedPath; } + set { this._referencedPath = value; } + } + + protected override GraphicsPath GetBaselinePath(ISvgRenderer renderer) + { + var path = this.OwnerDocument.IdManager.GetElementById(this.ReferencedPath) as SvgVisualElement; + if (path == null) return null; + var pathData = (GraphicsPath)path.Path(renderer).Clone(); + if (path.Transforms.Count > 0) + { + Matrix transformMatrix = new Matrix(1, 0, 0, 1, 0, 0); + + foreach (var transformation in path.Transforms) + { + transformMatrix.Multiply(transformation.Matrix); + } + + pathData.Transform(transformMatrix); + } + return pathData; + } + protected override float GetAuthorPathLength() + { + var path = this.OwnerDocument.IdManager.GetElementById(this.ReferencedPath) as SvgPath; + if (path == null) return 0; + return path.PathLength; + } + + public override SvgElement DeepCopy() + { + return base.DeepCopy(); + } + + + + + } +} diff --git a/Source/Text/SvgTextRef.cs b/Source/Text/SvgTextRef.cs new file mode 100644 index 0000000..45a878d --- /dev/null +++ b/Source/Text/SvgTextRef.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Svg +{ + [SvgElement("tref")] + public class SvgTextRef : SvgTextBase + { + private Uri _referencedElement; + + [SvgAttribute("href", SvgAttributeAttribute.XLinkNamespace)] + public virtual Uri ReferencedElement + { + get { return this._referencedElement; } + set { this._referencedElement = value; } + } + + internal override IEnumerable GetContentNodes() + { + var refText = this.OwnerDocument.IdManager.GetElementById(this.ReferencedElement) as SvgTextBase; + if (refText == null) + { + return base.GetContentNodes(); + } + else + { + return refText.GetContentNodes(); + } + } + + public override SvgElement DeepCopy() + { + return DeepCopy(); + } + + public override SvgElement DeepCopy() + { + var newObj = base.DeepCopy() as SvgTextRef; + newObj.X = this.X; + newObj.Y = this.Y; + newObj.Dx = this.Dx; + newObj.Dy = this.Dy; + newObj.Text = this.Text; + newObj.ReferencedElement = this.ReferencedElement; + + return newObj; + } + + + } +} diff --git a/Source/Transforms/ISvgTransformable.cs b/Source/Transforms/ISvgTransformable.cs index ed1a63e..348c049 100644 --- a/Source/Transforms/ISvgTransformable.cs +++ b/Source/Transforms/ISvgTransformable.cs @@ -18,14 +18,14 @@ namespace Svg /// SvgTransformCollection Transforms { get; set; } /// - /// Applies the required transforms to . + /// Applies the required transforms to . /// - /// The to be transformed. - void PushTransforms(SvgRenderer renderer); + /// The to be transformed. + void PushTransforms(ISvgRenderer renderer); /// - /// Removes any previously applied transforms from the specified . + /// Removes any previously applied transforms from the specified . /// - /// The that should have transforms removed. - void PopTransforms(SvgRenderer renderer); + /// The that should have transforms removed. + void PopTransforms(ISvgRenderer renderer); } } \ No newline at end of file diff --git a/Tests/SvgW3CTestRunner/View.cs b/Tests/SvgW3CTestRunner/View.cs index c39a31d..c8faec4 100644 --- a/Tests/SvgW3CTestRunner/View.cs +++ b/Tests/SvgW3CTestRunner/View.cs @@ -5,6 +5,7 @@ using System.Windows.Forms; using System.Drawing; using System.IO; using Svg; +using System.Diagnostics; namespace SvgW3CTestRunner { diff --git a/Tests/W3CTestSuite/png/__issue-015-01.png b/Tests/W3CTestSuite/png/__issue-015-01.png new file mode 100644 index 0000000000000000000000000000000000000000..3d8dd4a83eed5886fe2e1a8cbaa06e59115e42e5 GIT binary patch literal 6517 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 G?ZSUr|H`@m literal 0 HcmV?d00001 diff --git a/Tests/W3CTestSuite/png/__issue-064-02.png b/Tests/W3CTestSuite/png/__issue-064-02.png new file mode 100644 index 0000000000000000000000000000000000000000..c014043602423404ab304c74f9c0c23519286a6d GIT binary patch literal 2242 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 QeE + Created with Highcharts 3.0.2 + + + + + + + + + \ No newline at end of file diff --git a/Tests/W3CTestSuite/svg/__issue-34-02.svg b/Tests/W3CTestSuite/svg/__issue-034-02.svg similarity index 100% rename from Tests/W3CTestSuite/svg/__issue-34-02.svg rename to Tests/W3CTestSuite/svg/__issue-034-02.svg diff --git a/Tests/W3CTestSuite/svg/__issue-036-01.svg b/Tests/W3CTestSuite/svg/__issue-036-01.svg new file mode 100644 index 0000000..cbd0373 --- /dev/null +++ b/Tests/W3CTestSuite/svg/__issue-036-01.svg @@ -0,0 +1 @@ +Created with Highcharts 3.0.1DestinationCountDestination Summary8587746333129221UnknownHospital 2Clinic AAbacusHospital 3Hospital 1ABCD RegionalMed CtrHospital 501000250500750 \ No newline at end of file diff --git a/Tests/W3CTestSuite/svg/__issue-064-01.svg b/Tests/W3CTestSuite/svg/__issue-064-01.svg new file mode 100644 index 0000000..a55bb61 --- /dev/null +++ b/Tests/W3CTestSuite/svg/__issue-064-01.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/W3CTestSuite/svg/__issue-064-02.svg b/Tests/W3CTestSuite/svg/__issue-064-02.svg new file mode 100644 index 0000000..f29eae3 --- /dev/null +++ b/Tests/W3CTestSuite/svg/__issue-064-02.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/W3CTestSuite/svg/__issue-83-01.svg b/Tests/W3CTestSuite/svg/__issue-083-01.svg similarity index 100% rename from Tests/W3CTestSuite/svg/__issue-83-01.svg rename to Tests/W3CTestSuite/svg/__issue-083-01.svg diff --git a/Tests/W3CTestSuite/svg/text-path-02-b.svg b/Tests/W3CTestSuite/svg/text-path-02-b.svg index 3513226..1e00df5 100644 --- a/Tests/W3CTestSuite/svg/text-path-02-b.svg +++ b/Tests/W3CTestSuite/svg/text-path-02-b.svg @@ -63,26 +63,26 @@ - Positive offset Negative offset + Negative offset - Positive offset Negative offset + Negative offset - Positive offset Negative offset + Positive offset - Positive offset Negative offset + Positive offset -- GitLab