diff --git a/Samples/Example/Example.csproj b/Samples/Example/Example.csproj
index def0b7f..cc2f2ba 100644
--- a/Samples/Example/Example.csproj
+++ b/Samples/Example/Example.csproj
@@ -43,9 +43,11 @@
+
+
diff --git a/Samples/Example/Images/acid1_TextOnly.svg b/Samples/Example/Images/acid1_TextOnly.svg
new file mode 100644
index 0000000..0ef7b64
--- /dev/null
+++ b/Samples/Example/Images/acid1_TextOnly.svg
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Example/Images/boldgreen.svg b/Samples/Example/Images/boldgreen.svg
new file mode 100644
index 0000000..e9edc0f
--- /dev/null
+++ b/Samples/Example/Images/boldgreen.svg
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/Source/SVGImage/DotNetProjects.SVGImage.csproj b/Source/SVGImage/DotNetProjects.SVGImage.csproj
index 02886db..414995a 100644
--- a/Source/SVGImage/DotNetProjects.SVGImage.csproj
+++ b/Source/SVGImage/DotNetProjects.SVGImage.csproj
@@ -29,7 +29,7 @@
images\dotnetprojects.png
svg wpf svg-icons svg-to-png svg-to-xaml svgimage svgimage-control
Readme.md
- true
+
5.1.0
diff --git a/Source/SVGImage/SVG/PaintServers/PaintServerManager.cs b/Source/SVGImage/SVG/PaintServers/PaintServerManager.cs
index d5dc1cc..b1159af 100644
--- a/Source/SVGImage/SVG/PaintServers/PaintServerManager.cs
+++ b/Source/SVGImage/SVG/PaintServers/PaintServerManager.cs
@@ -160,10 +160,11 @@ public static Color ParseHexColor(string value)
newval |= (@int & 0x00000f);
u = newval;
}
- else {
- u = Convert.ToUInt64(value.Substring(start), 16);
- }
-
+ else
+ {
+ u = Convert.ToUInt64(value.Substring(start), 16);
+ }
+
byte a = (byte)((u & 0xff000000) >> 24);
byte r = (byte)((u & 0x00ff0000) >> 16);
byte g = (byte)((u & 0x0000ff00) >> 8);
@@ -223,10 +224,12 @@ private int ParseColorNumber(string value)
{
if (value.EndsWith("%"))
{
- var nr = int.Parse(value.Substring(0, value.Length - 1));
+ var nr = double.Parse(value.Substring(0, value.Length - 1));
if (nr < 0)
- nr = 255 - nr;
- return nr * 255 / 100;
+ nr = 255 - nr; //TODO: what is this trying to do?
+ var result = (int)Math.Round((nr * 255) / 100);
+
+ return MathUtil.Clamp(result, 0, 255);
}
return int.Parse(value);
diff --git a/Source/SVGImage/SVG/SVGRender.cs b/Source/SVGImage/SVG/SVGRender.cs
index 7039d93..24b1c64 100644
--- a/Source/SVGImage/SVG/SVGRender.cs
+++ b/Source/SVGImage/SVG/SVGRender.cs
@@ -3,6 +3,7 @@
using System.Xml;
using System.Linq;
using System.Collections.Generic;
+using SVGImage.SVG.Utils;
using System.Windows;
using System.Windows.Media;
@@ -115,7 +116,8 @@ public DrawingGroup LoadDrawing(Stream stream)
public DrawingGroup CreateDrawing(SVG svg)
{
- return this.LoadGroup(svg.Elements, svg.ViewBox, false);
+ var drawingGroup = this.LoadGroup(svg.Elements, svg.ViewBox, false);
+ return drawingGroup;
}
public DrawingGroup CreateDrawing(Shape shape)
@@ -443,9 +445,9 @@ internal DrawingGroup LoadGroup(IList elements, Rect? viewBox, bool isSwi
GeometryGroup gp = textRender2.BuildTextGeometry(textShape);
if (gp != null)
{
- foreach (Geometry gm in gp.Children)
+ foreach (Geometry gm in GetStyledSpans(gp))
{
- if (TextRenderBase.GetElement(gm) is TextShapeBase tspan)
+ if (TextRender.GetElement(gm) is TextShapeBase tspan)
{
var di = this.NewDrawingItem(tspan, gm);
AddDrawingToGroup(grp, shape, di);
@@ -480,6 +482,31 @@ internal DrawingGroup LoadGroup(IList elements, Rect? viewBox, bool isSwi
return grp;
}
+ private static IEnumerable GetStyledSpans(Geometry geometry)
+ {
+ if (geometry is GeometryGroup gg)
+ {
+ if (!(TextRender.GetElement(gg) is null))
+ {
+ yield return geometry;
+ }
+ else
+ {
+ foreach (var g in gg.Children)
+ {
+ foreach (var subg in GetStyledSpans(g))
+ {
+ yield return subg;
+ }
+ }
+ }
+ }
+ else
+ {
+ yield return geometry;
+ }
+ }
+
private void AddDrawingToGroup(DrawingGroup grp, Shape shape, Drawing drawing)
{
if (shape.Clip != null || shape.Transform != null || shape.Filter != null)
diff --git a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs
index 4b9ca9b..a255c51 100644
--- a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs
+++ b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs
@@ -6,7 +6,7 @@ namespace SVGImage.SVG.Shapes
public struct LengthPercentageOrNumber
{
- private static readonly Regex _lengthRegex = new Regex(@"(?\d+(?:\.\d+)?)\s*(?%|\w+)?", RegexOptions.Compiled | RegexOptions.Singleline);
+ private static readonly Regex _lengthRegex = new Regex(@"(?-?\d+(?:\.\d+)?)\s*(?%|\w+)?", RegexOptions.Compiled | RegexOptions.Singleline);
private readonly LengthContext _context;
private readonly double _value;
///
@@ -180,12 +180,12 @@ public static LengthPercentageOrNumber Parse(Shape owner, string value, LengthOr
}
else
{
- // Default to pixels if no unit is specified
- context = new LengthContext(owner, LengthUnit.px);
+ // Default to Number if no unit is specified
+ context = new LengthContext(owner, LengthUnit.Number);
}
return new LengthPercentageOrNumber(d, context);
}
-
+
}
diff --git a/Source/SVGImage/SVG/Shapes/Shape.cs b/Source/SVGImage/SVG/Shapes/Shape.cs
index 7d45c29..a44f6ed 100644
--- a/Source/SVGImage/SVG/Shapes/Shape.cs
+++ b/Source/SVGImage/SVG/Shapes/Shape.cs
@@ -125,6 +125,11 @@ public Stroke Stroke
}
protected virtual Fill DefaultFill()
+ {
+ return null;
+ }
+
+ protected virtual Fill GetParentFill()
{
var parent = this.Parent;
while (parent != null)
@@ -141,7 +146,7 @@ protected virtual Fill DefaultFill()
public Fill Fill
{
- get => m_fill ?? DefaultFill();
+ get => m_fill ?? GetParentFill() ?? DefaultFill();
set => m_fill = value;
}
diff --git a/Source/SVGImage/SVG/Shapes/TextRender.cs b/Source/SVGImage/SVG/Shapes/TextRender.cs
index cdd1a12..03fb88c 100644
--- a/Source/SVGImage/SVG/Shapes/TextRender.cs
+++ b/Source/SVGImage/SVG/Shapes/TextRender.cs
@@ -7,6 +7,9 @@ namespace SVGImage.SVG.Shapes
using Utils;
using System.Linq;
using System.Windows.Markup;
+ using System;
+ using System.Reflection;
+ using System.Windows.Documents;
///
/// This class is responsible for rendering text shapes in SVG.
@@ -25,6 +28,117 @@ public override GeometryGroup BuildTextGeometry(TextShape text)
return CreateGeometry(text, state);
}
}
+
+
+
+ public static readonly DependencyProperty TextSpanTextStyleProperty = DependencyProperty.RegisterAttached(
+ "TextSpanTextStyle", typeof(TextStyle), typeof(DependencyObject));
+ private static void SetTextSpanTextStyle(DependencyObject obj, TextStyle value)
+ {
+ obj.SetValue(TextSpanTextStyleProperty, value);
+ }
+ public static TextStyle GetTextSpanTextStyle(DependencyObject obj)
+ {
+ return (TextStyle)obj.GetValue(TextSpanTextStyleProperty);
+ }
+
+ private sealed class TextChunk
+ {
+ public List GlyphRuns { get; set; } = new List();
+ public Dictionary> BackgroundDecorations { get; set; } = new Dictionary>();
+ public Dictionary> ForegroundDecorations { get; set; } = new Dictionary>();
+ public Dictionary GlyphRunBounds { get; set; } = new Dictionary();
+ public Dictionary TextStyles { get; set; } = new Dictionary();
+ public Dictionary TextContainers { get; set; } = new Dictionary();
+ public TextAlignment TextAlignment { get; set; }
+
+ public GeometryGroup BuildGeometry()
+ {
+ double alignmentOffset = GetAlignmentOffset();
+ bool nonZeroAlignmentOffset = !alignmentOffset.IsNearlyZero();
+ GeometryGroup geometryGroup = new GeometryGroup();
+ foreach (var glyphRun in GlyphRuns)
+ {
+ var runGeometry = !nonZeroAlignmentOffset ? glyphRun.BuildGeometry() : glyphRun.CreateOffsetRun(alignmentOffset, 0).BuildGeometry();
+ geometryGroup.Children.Add(runGeometry);
+ if (TextStyles.TryGetValue(glyphRun, out TextStyle textStyle))
+ {
+ TextRender.SetTextSpanTextStyle(runGeometry, textStyle);
+ }
+ if (TextContainers.TryGetValue(glyphRun, out TextShapeBase textContainer))
+ {
+ TextRender.SetElement(runGeometry, textContainer);
+ }
+ if (BackgroundDecorations.TryGetValue(glyphRun, out List backgroundDecorations))
+ {
+ foreach (var decoration in backgroundDecorations)
+ {
+ if (nonZeroAlignmentOffset)
+ {
+ decoration.Offset(alignmentOffset, 0);
+ }
+ //Underline and OverLine should be drawn behind the text
+ geometryGroup.Children.Insert(0, new RectangleGeometry(decoration));
+ }
+ }
+ if (ForegroundDecorations.TryGetValue(glyphRun, out List foregroundDecorations))
+ {
+ foreach (var decoration in foregroundDecorations)
+ {
+ if (nonZeroAlignmentOffset)
+ {
+ decoration.Offset(alignmentOffset, 0);
+ }
+ //Strikethrough should be drawn on top of the text
+ geometryGroup.Children.Add(new RectangleGeometry(decoration));
+ }
+ }
+ }
+ return geometryGroup;
+ }
+
+ private Rect GetBoundingBox()
+ {
+ if (GlyphRunBounds.Count == 0)
+ {
+ return Rect.Empty;
+ }
+ Rect boundingBox = GlyphRunBounds.First().Value;
+ foreach (var kvp in GlyphRunBounds)
+ {
+ boundingBox.Union(kvp.Value);
+ }
+ return boundingBox;
+ }
+
+ private double GetAlignmentOffset()
+ {
+ if(TextAlignment == TextAlignment.Left)
+ {
+ return 0; // No offset needed for left alignment
+ }
+ var boundingBox = GetBoundingBox();
+ double totalWidth = boundingBox.Width;
+ double alignmentOffset = 0;
+ switch (TextAlignment)
+ {
+ case TextAlignment.Left:
+ break;
+ case TextAlignment.Right:
+ alignmentOffset = totalWidth;
+ break;
+ case TextAlignment.Center:
+ alignmentOffset = totalWidth / 2d;
+ break;
+ case TextAlignment.Justify:
+ // Justify is not implemented
+ break;
+ default:
+ break;
+ }
+ return alignmentOffset;
+ }
+ }
private static GeometryGroup CreateGeometry(TextShape root, TextRenderState state)
{
state.Resolve(root);
@@ -35,33 +149,57 @@ private static GeometryGroup CreateGeometry(TextShape root, TextRenderState stat
GeometryGroup mainGeometryGroup = new GeometryGroup();
var baselineOrigin = new Point(root.X.FirstOrDefault().Value, root.Y.FirstOrDefault().Value);
var isSideways = root.WritingMode == WritingMode.HorizontalTopToBottom;
+ TextAlignment currentTextAlignment = root.TextStyle.TextAlignment;
+ List textChunks = new List();
+ bool newTextChunk = false;
+ TextChunk currentTextChunk = null;
foreach (TextString textString in textStrings)
{
- GeometryGroup geometryGroup = new GeometryGroup();
var textStyle = textString.TextStyle;
Typeface font = textString.TextStyle.GetTypeface();
- if (CreateRun(textString, state, font, isSideways, baselineOrigin, out Point newBaseline) is GlyphRun run)
+ if (CreateRun(textString, state, font, isSideways, baselineOrigin, out Point newBaseline, out newTextChunk, ref currentTextAlignment) is GlyphRun run)
{
+ if (newTextChunk)
+ {
+ if(currentTextChunk != null)
+ {
+ // Add the current text chunk to the list
+ textChunks.Add(currentTextChunk);
+ }
+ currentTextChunk = new TextChunk();
+ currentTextChunk.TextAlignment = currentTextAlignment;
+ }
var runGeometry = run.BuildGeometry();
- geometryGroup.Children.Add(runGeometry);
+ currentTextChunk.GlyphRuns.Add(run);
+ currentTextChunk.TextStyles[run] = textStyle;
+ currentTextChunk.GlyphRunBounds[run] = runGeometry.Bounds;
+ currentTextChunk.TextContainers[run] = (TextShapeBase)textString.Parent;
if (textStyle.TextDecoration != null && textStyle.TextDecoration.Count > 0)
{
- GetTextDecorations(geometryGroup, textStyle, font, baselineOrigin, out List backgroundDecorations, out List foregroundDecorations);
- foreach (var decoration in backgroundDecorations)
+ GetTextDecorations(runGeometry, textStyle, font, baselineOrigin, out List backgroundDecorations, out List foregroundDecorations);
+ if(backgroundDecorations.Count > 0)
{
- //Underline and OverLine should be drawn behind the text
- geometryGroup.Children.Insert(0, new RectangleGeometry(decoration));
+ currentTextChunk.BackgroundDecorations[run] = backgroundDecorations;
}
- foreach (var decoration in foregroundDecorations)
+ if (foregroundDecorations.Count > 0)
{
- //Strikethrough should be drawn on top of the text
- geometryGroup.Children.Add(new RectangleGeometry(decoration));
+ currentTextChunk.ForegroundDecorations[run] = foregroundDecorations;
}
}
- mainGeometryGroup.Children.Add(geometryGroup);
}
baselineOrigin = newBaseline;
}
+ textChunks.Add(currentTextChunk);
+
+ foreach(var textChunk in textChunks)
+ {
+ if (textChunk.GlyphRuns.Count == 0)
+ {
+ continue; // No glyphs to render in this chunk
+ }
+ GeometryGroup geometryGroup = textChunk.BuildGeometry();
+ mainGeometryGroup.Children.Add(geometryGroup);
+ }
mainGeometryGroup.Transform = root.Transform;
return mainGeometryGroup;
@@ -73,13 +211,13 @@ private static GeometryGroup CreateGeometry(TextShape root, TextRenderState stat
///
/// Not perfect, the lines are not continuous across multiple text strings.
///
- ///
+ ///
///
///
///
///
///
- private static void GetTextDecorations(GeometryGroup geometryGroup, TextStyle textStyle, Typeface font, Point baselineOrigin, out List backgroundDecorations, out List foregroundDecorations)
+ private static void GetTextDecorations(Geometry geometry, TextStyle textStyle, Typeface font, Point baselineOrigin, out List backgroundDecorations, out List foregroundDecorations)
{
backgroundDecorations = new List();
foregroundDecorations = new List();
@@ -93,28 +231,29 @@ private static void GetTextDecorations(GeometryGroup geometryGroup, TextStyle te
{
decorationPos = baselineY - (font.StrikethroughPosition * fontSize);
decorationThickness = font.StrikethroughThickness * fontSize;
- Rect bounds = new Rect(geometryGroup.Bounds.Left, decorationPos, geometryGroup.Bounds.Width, decorationThickness);
+ Rect bounds = new Rect(geometry.Bounds.Left, decorationPos, geometry.Bounds.Width, decorationThickness);
foregroundDecorations.Add(bounds);
}
else if (textDecorationLocation == TextDecorationLocation.Underline)
{
decorationPos = baselineY - (font.UnderlinePosition * fontSize);
decorationThickness = font.UnderlineThickness * fontSize;
- Rect bounds = new Rect(geometryGroup.Bounds.Left, decorationPos, geometryGroup.Bounds.Width, decorationThickness);
+ Rect bounds = new Rect(geometry.Bounds.Left, decorationPos, geometry.Bounds.Width, decorationThickness);
backgroundDecorations.Add(bounds);
}
else if (textDecorationLocation == TextDecorationLocation.OverLine)
{
decorationPos = baselineY - fontSize;
decorationThickness = font.StrikethroughThickness * fontSize;
- Rect bounds = new Rect(geometryGroup.Bounds.Left, decorationPos, geometryGroup.Bounds.Width, decorationThickness);
+ Rect bounds = new Rect(geometry.Bounds.Left, decorationPos, geometry.Bounds.Width, decorationThickness);
backgroundDecorations.Add(bounds);
}
}
}
- private static GlyphRun CreateRun(TextString textString, TextRenderState state, Typeface font, bool isSideways, Point baselineOrigin, out Point newBaseline)
+ private static GlyphRun CreateRun(TextString textString, TextRenderState state, Typeface font, bool isSideways, Point baselineOrigin, out Point newBaseline, out bool newTextChunk, ref TextAlignment currentTextAlignment)
{
+ newTextChunk = textString.FirstCharacter.GlobalIndex == 0;
var textStyle = textString.TextStyle;
var characterInfos = textString.GetCharacters();
if(characterInfos is null ||characterInfos.Length == 0)
@@ -135,31 +274,64 @@ private static GlyphRun CreateRun(TextString textString, TextRenderState state,
var renderingEmSize = textStyle.FontSize;
var characters = characterInfos.Select(c => c.Character).ToArray();
var glyphIndices = characters.Select(c => glyphFace.CharacterToGlyphMap[c]).ToList();
- var advanceWidths = glyphIndices.Select(c => glyphFace.AdvanceWidths[c] * renderingEmSize).ToArray();
-
+ var advanceWidths = WrapInThousandthOfEmRealDoubles(renderingEmSize, glyphIndices.Select(c => glyphFace.AdvanceWidths[c] * renderingEmSize).ToArray());
if (characterInfos[0].DoesPositionX)
{
baselineOrigin.X = characterInfos[0].X;
+ newTextChunk = true;
}
if (characterInfos[0].DoesPositionY)
{
baselineOrigin.Y = characterInfos[0].Y;
+ newTextChunk = true;
}
+ else if(textString.TextStyle.TextAlignment != currentTextAlignment)
+ {
+ newTextChunk = true;
+ }
+
+ double baselineShift = 0;
+ baselineShift += BaselineHelper.EstimateBaselineShiftValue(textStyle, textString.Parent);
+ //if (textStyle.BaseLineShift == "sub")
+ //{
+ // baselineShift += textStyle.FontSize * 0.5; /* * cap height ? fontSize*/
+ //}
+ //else if (textStyle.BaseLineShift == "super")
+ //{
+ // baselineShift -= textStyle.FontSize + (textStyle.FontSize * 0.25)/*font.CapsHeight * fontSize*/;
+ //}
+
+ double totalWidth = advanceWidths.Sum();
+
GlyphRun run = new GlyphRun(glyphFace, state.BidiLevel, isSideways, renderingEmSize,
#if !DOTNET40 && !DOTNET45 && !DOTNET46
(float)DpiUtil.PixelsPerDip,
#endif
- glyphIndices, baselineOrigin, advanceWidths, glyphOffsets, characters, deviceFontName, clusterMap, caretStops, language);
-
- var newX = baselineOrigin.X + advanceWidths.Sum();
+ glyphIndices, new Point(baselineOrigin.X, baselineOrigin.Y + baselineShift), advanceWidths, glyphOffsets, characters, deviceFontName, clusterMap, caretStops, language);
+
+ var newX = baselineOrigin.X + totalWidth;
var newY = baselineOrigin.Y ;
newBaseline = new Point(newX, newY);
return run;
}
-
+
+ private static readonly Type _thousandthOfEmRealDoublesType = Type.GetType("MS.Internal.TextFormatting.ThousandthOfEmRealDoubles, PresentationCore, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
+ private static readonly ConstructorInfo _thousandthOfEmRealDoublesConstructor = _thousandthOfEmRealDoublesType?.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(double), typeof(IList)}, null);
+
+ ///
+ /// Microsoft's GlyphRun converts the advance widths to thousandths of an em when using EndInit.
+ /// This wrapper method is used to compare apples to apples
+ ///
+ ///
+ ///
+ ///
+ private static IList WrapInThousandthOfEmRealDoubles(double renderingEmSize, IList advanceWidths)
+ {
+ return (IList)_thousandthOfEmRealDoublesConstructor?.Invoke(new object[] { renderingEmSize, advanceWidths }) ?? advanceWidths;
+ }
private static void PopulateTextStrings(List textStrings, ITextNode node, TextStyleStack textStyleStacks)
diff --git a/Source/SVGImage/SVG/Shapes/TextRenderBase.cs b/Source/SVGImage/SVG/Shapes/TextRenderBase.cs
index ccec0cf..47c619b 100644
--- a/Source/SVGImage/SVG/Shapes/TextRenderBase.cs
+++ b/Source/SVGImage/SVG/Shapes/TextRenderBase.cs
@@ -7,14 +7,14 @@ public abstract class TextRenderBase
{
public abstract GeometryGroup BuildTextGeometry(TextShape text);
public static DependencyProperty TSpanElementProperty = DependencyProperty.RegisterAttached(
- "TSpanElement", typeof(ITextNode), typeof(DependencyObject));
- public static void SetElement(DependencyObject obj, ITextNode value)
+ "TSpanElement", typeof(TextShapeBase), typeof(DependencyObject));
+ public static void SetElement(DependencyObject obj, TextShapeBase value)
{
obj.SetValue(TSpanElementProperty, value);
}
- public static ITextNode GetElement(DependencyObject obj)
+ public static TextShapeBase GetElement(DependencyObject obj)
{
- return (ITextNode)obj.GetValue(TSpanElementProperty);
+ return (TextShapeBase)obj.GetValue(TSpanElementProperty);
}
}
diff --git a/Source/SVGImage/SVG/Shapes/TextShape.cs b/Source/SVGImage/SVG/Shapes/TextShape.cs
index da28c7f..29e5b59 100644
--- a/Source/SVGImage/SVG/Shapes/TextShape.cs
+++ b/Source/SVGImage/SVG/Shapes/TextShape.cs
@@ -1,4 +1,5 @@
-using System.Xml;
+using System.Linq;
+using System.Xml;
namespace SVGImage.SVG.Shapes
{
@@ -6,7 +7,87 @@ public class TextShape : TextShapeBase
{
public TextShape(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent)
{
-
+ CollapseWhitespaceBetweenTextNodes(this);
+ }
+ private static bool EndsWithWhitespace(string text)
+ {
+ return !string.IsNullOrEmpty(text) && char.IsWhiteSpace(text[text.Length - 1]);
+ }
+
+ private static bool StartsWithWhitespace(string text)
+ {
+ return !string.IsNullOrEmpty(text) && char.IsWhiteSpace(text[0]);
+ }
+ private static TextString GetFirstLeaf(ITextNode node)
+ {
+ if (node is TextString leaf)
+ {
+ return leaf;
+ }
+
+ if (node is TextShapeBase container)
+ {
+ foreach (var child in container.Children)
+ {
+ var result = GetFirstLeaf(child);
+ if (!(result is null))
+ {
+ return result;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static TextString GetLastLeaf(ITextNode node)
+ {
+ if (node is TextString leaf)
+ {
+ return leaf;
+ }
+
+ if (node is TextShapeBase container)
+ {
+ for (int i = container.Children.Count - 1; i >= 0; i--)
+ {
+ var result = GetLastLeaf(container.Children[i]);
+ if (!(result is null))
+ {
+ return result;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static void CollapseWhitespaceBetweenTextNodes(TextShape root)
+ {
+ TextString previousLeaf = null;
+ CollapseWhitespaceBetweenTextNodes(root, ref previousLeaf);
+ }
+
+ private static void CollapseWhitespaceBetweenTextNodes(ITextNode node, ref TextString previousLeaf)
+ {
+ if (node is TextString currentLeaf)
+ {
+ if (!(previousLeaf is null) &&
+ EndsWithWhitespace(previousLeaf.Text) &&
+ StartsWithWhitespace(currentLeaf.Text))
+ {
+ currentLeaf.Characters = currentLeaf.Characters.Skip(1).ToArray();
+ }
+
+ previousLeaf = currentLeaf;
+ }
+ else if (node is TextShapeBase container)
+ {
+ foreach (var child in container.Children)
+ {
+ CollapseWhitespaceBetweenTextNodes(child, ref previousLeaf);
+ }
+ }
}
}
diff --git a/Source/SVGImage/SVG/Shapes/TextShapeBase.cs b/Source/SVGImage/SVG/Shapes/TextShapeBase.cs
index 00e2053..43e5b7b 100644
--- a/Source/SVGImage/SVG/Shapes/TextShapeBase.cs
+++ b/Source/SVGImage/SVG/Shapes/TextShapeBase.cs
@@ -7,6 +7,7 @@ namespace SVGImage.SVG.Shapes
using System.Linq;
using System.Diagnostics;
using System.Text;
+ using System.Text.RegularExpressions;
[DebuggerDisplay("{DebugDisplayText}")]
public class TextShapeBase: Shape, ITextNode
@@ -39,7 +40,15 @@ private string GetDebugDisplayText(StringBuilder sb)
return sb.ToString();
}
-
+
+ protected override Fill DefaultFill()
+ {
+ return Fill.CreateDefault(Svg, "black");
+ }
+ protected override Stroke DefaultStroke()
+ {
+ return Stroke.CreateDefault(Svg, 0.1);
+ }
public LengthPercentageOrNumberList X { get; protected set; }
public LengthPercentageOrNumberList Y { get; protected set; }
@@ -173,6 +182,7 @@ protected override void ParseAtStart(SVG svg, XmlNode node)
ParseChildren(svg, node);
}
+ private static readonly Regex _trimmedWhitespace = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.Singleline);
protected void ParseChildren(SVG svg, XmlNode node)
{
@@ -180,7 +190,7 @@ protected void ParseChildren(SVG svg, XmlNode node)
{
if (child.NodeType == XmlNodeType.Text || child.NodeType == XmlNodeType.CDATA)
{
- var text = child.InnerText.Trim();
+ var text = _trimmedWhitespace.Replace(child.InnerText, " ");
if (!string.IsNullOrWhiteSpace(text))
{
Children.Add(new TextString(this, text));
diff --git a/Source/SVGImage/SVG/Shapes/TextString.cs b/Source/SVGImage/SVG/Shapes/TextString.cs
index 7b0af19..4dc907f 100644
--- a/Source/SVGImage/SVG/Shapes/TextString.cs
+++ b/Source/SVGImage/SVG/Shapes/TextString.cs
@@ -23,7 +23,7 @@ public class TextString : ITextChild
public TextString(Shape parent, string text)
{
Parent = parent;
- string trimmed = _trimmedWhitespace.Replace(text.Trim(), " ");
+ string trimmed = _trimmedWhitespace.Replace(text, " ");
Characters = new CharacterLayout[trimmed.Length];
for(int i = 0; i < trimmed.Length; i++)
{
diff --git a/Source/SVGImage/SVG/TextRender2.cs b/Source/SVGImage/SVG/TextRender2.cs
index b9289d9..537121d 100644
--- a/Source/SVGImage/SVG/TextRender2.cs
+++ b/Source/SVGImage/SVG/TextRender2.cs
@@ -51,7 +51,7 @@ static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle,
baseline -= tspan.TextStyle.FontSize + (textString.TextStyle.FontSize * 0.25)/*font.CapsHeight * fontSize*/;
Geometry gm = BuildGlyphRun(textString.TextStyle, txt, x, baseline, ref totalwidth);
- TextRender2.SetElement(gm, textString);
+ TextRender2.SetElement(gm, (TextShapeBase)textString.Parent);
gp.Children.Add(gm);
x += totalwidth;
}
diff --git a/Source/SVGImage/SVG/Utils/BaselineHelper.cs b/Source/SVGImage/SVG/Utils/BaselineHelper.cs
new file mode 100644
index 0000000..0896a16
--- /dev/null
+++ b/Source/SVGImage/SVG/Utils/BaselineHelper.cs
@@ -0,0 +1,66 @@
+using SVGImage.SVG.Shapes;
+using System;
+
+namespace SVGImage.SVG.Utils
+{
+ internal static class BaselineHelper
+ {
+ public static LengthPercentageOrNumber EstimateBaselineShift(Shape shape)
+ {
+ return EstimateBaselineShift(shape.TextStyle, shape);
+ }
+ ///
+ /// The purpose of this method is to allow TextStrings which are not shapes themselves to use the same logic as TextShapes to estimate the baseline shift.
+ /// They can use this method to estimate the baseline shift based on the TextStyle of the TextString's parent Shape.
+ ///
+ ///
+ ///
+ ///
+ public static LengthPercentageOrNumber EstimateBaselineShift(TextStyle textStyle, Shape shape)
+ {
+ if (String.IsNullOrEmpty( textStyle.BaseLineShift) || textStyle.BaseLineShift == "baseline")
+ {
+ return new LengthPercentageOrNumber(0d, new LengthContext(shape, LengthUnit.Number));
+ }
+ else if (textStyle.BaseLineShift == "sub")
+ {
+ //Based on previous estimation
+ return new LengthPercentageOrNumber( textStyle.FontSize * 0.5, new LengthContext(shape, LengthUnit.Number));
+ }
+ else if (textStyle.BaseLineShift == "super")
+ {
+ //Based on previous estimation
+ return new LengthPercentageOrNumber((-1) * (textStyle.FontSize + (textStyle.FontSize * 0.25)), new LengthContext(shape, LengthUnit.Number));
+ }
+ else if(textStyle.BaseLineShift.EndsWith("%") && Double.TryParse(textStyle.BaseLineShift.Substring(0, textStyle.BaseLineShift.Length - 1), out double d))
+ {
+ try
+ {
+ //The computed value of the property is this percentage multiplied by the computed "line-height" of the ‘text’ element.
+ //for the purposes of processing the ‘font’ property in SVG, 'line-height' is assumed to be equal the value for property ‘font-size’
+ return new LengthPercentageOrNumber(d, new LengthContext(shape, LengthUnit.rem));
+ }
+ catch
+ {
+ //Continue
+ }
+ }
+ try
+ {
+ return LengthPercentageOrNumber.Parse(shape, textStyle.BaseLineShift, LengthOrientation.Vertical);
+ }
+ catch
+ {
+ return new LengthPercentageOrNumber(0d, new LengthContext(shape, LengthUnit.Number));
+ }
+ }
+ public static double EstimateBaselineShiftValue(Shape shape)
+ {
+ return EstimateBaselineShift(shape).Value;
+ }
+ public static double EstimateBaselineShiftValue(TextStyle textStyle, Shape shape)
+ {
+ return EstimateBaselineShift(textStyle, shape).Value;
+ }
+ }
+}
diff --git a/Source/SVGImage/SVG/Utils/DrawingGroupSerializer.cs b/Source/SVGImage/SVG/Utils/DrawingGroupSerializer.cs
new file mode 100644
index 0000000..db0478e
--- /dev/null
+++ b/Source/SVGImage/SVG/Utils/DrawingGroupSerializer.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Windows.Media;
+using System.Windows.Markup;
+using System.IO;
+using System.Xml;
+
+namespace SVGImage.SVG.Utils
+{
+ internal static class DrawingGroupSerializer
+ {
+ public static string SerializeToXaml(DrawingGroup drawingGroup)
+ {
+ if (drawingGroup is null)
+ {
+ throw new ArgumentNullException(nameof(drawingGroup));
+ }
+
+ // Freezing can help ensure serialization works without exceptions
+ if (drawingGroup.CanFreeze && !drawingGroup.IsFrozen)
+ {
+ drawingGroup.Freeze();
+ }
+
+ var settings = new XmlWriterSettings
+ {
+ Indent = true,
+ IndentChars = " ",
+ OmitXmlDeclaration = true
+ };
+
+ using (var stringWriter = new StringWriter())
+ using (var xmlWriter = XmlWriter.Create(stringWriter, settings))
+ {
+ XamlWriter.Save(drawingGroup, xmlWriter);
+ return stringWriter.ToString();
+ }
+ }
+ }
+}
diff --git a/Source/SVGImage/SVG/Utils/FontResolver.cs b/Source/SVGImage/SVG/Utils/FontResolver.cs
index a64db0b..f753675 100644
--- a/Source/SVGImage/SVG/Utils/FontResolver.cs
+++ b/Source/SVGImage/SVG/Utils/FontResolver.cs
@@ -13,7 +13,7 @@ namespace SVGImage.SVG.Utils
///
public class FontResolver
{
- private readonly ConcurrentDictionary _fontCache = new ConcurrentDictionary();
+ private readonly ConcurrentDictionary _fontCache = new ConcurrentDictionary();
private readonly Dictionary _availableFonts;
private readonly Dictionary _normalizedFontNameMap;
@@ -43,7 +43,7 @@ public FontResolver(int maxLevenshteinDistance = 0)
///
/// Attempts to a font family based on the requested font name.
///
- /// The name of the font to resolve.
+ /// The name of the font or fonts to resolve. Multiple fonts should be separated by commas
///
/// A if a match is found, otherwise null.
///
@@ -51,6 +51,27 @@ public FontResolver(int maxLevenshteinDistance = 0)
/// Thrown when the requested font name is null or empty.
///
public FontFamily ResolveFontFamily(string requestedFontName)
+ {
+ var fontList = requestedFontName
+ .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(font => ResolveFontFamilyInternal(font.Trim()))
+ .Where(font => font != null);
+
+ var resolvedFontNames = String.Join(",", fontList);
+ return new FontFamily(resolvedFontNames);
+ }
+
+ ///
+ /// Attempts to a font family based on the requested font name.
+ ///
+ /// The name of the font to resolve.
+ ///
+ /// A if a match is found, otherwise null.
+ ///
+ ///
+ /// Thrown when the requested font name is null or empty.
+ ///
+ private string ResolveFontFamilyInternal(string requestedFontName)
{
if (string.IsNullOrWhiteSpace(requestedFontName))
{
@@ -65,8 +86,8 @@ public FontFamily ResolveFontFamily(string requestedFontName)
// 1. Exact match
if (_availableFonts.TryGetValue(requestedFontName, out var exactMatch))
{
- _fontCache[requestedFontName] = exactMatch;
- return exactMatch;
+ _fontCache[requestedFontName] = exactMatch.Source;
+ return exactMatch.Source;
}
// 2. Normalized match
@@ -74,8 +95,8 @@ public FontFamily ResolveFontFamily(string requestedFontName)
if (_normalizedFontNameMap.TryGetValue(normalizedRequested, out var normalizedMatchName) &&
_availableFonts.TryGetValue(normalizedMatchName, out var normalizedMatch))
{
- _fontCache[requestedFontName] = normalizedMatch;
- return normalizedMatch;
+ _fontCache[requestedFontName] = normalizedMatch.Source;
+ return normalizedMatch.Source;
}
// 3. Substring match
@@ -83,8 +104,8 @@ public FontFamily ResolveFontFamily(string requestedFontName)
.FirstOrDefault(kv => Normalize(kv.Key).Contains(normalizedRequested));
if (substringMatch.Value != null)
{
- _fontCache[requestedFontName] = substringMatch.Value;
- return substringMatch.Value;
+ _fontCache[requestedFontName] = substringMatch.Value.Source;
+ return substringMatch.Value.Source;
}
// 4. Levenshtein match (optional but slow)
@@ -102,16 +123,14 @@ public FontFamily ResolveFontFamily(string requestedFontName)
if (bestMatch != null && bestMatch.Distance <= 4)
{
- _fontCache[requestedFontName] = bestMatch.Font;
- return bestMatch.Font;
+ _fontCache[requestedFontName] = bestMatch.Font.Source;
+ return bestMatch.Font.Source;
}
}
-
-
// 5. No match
- _fontCache[requestedFontName] = null;
- return null;
+ _fontCache[requestedFontName] = requestedFontName;
+ return requestedFontName;
}
///
diff --git a/Source/SVGImage/SVG/Utils/GlyphRunUtil.cs b/Source/SVGImage/SVG/Utils/GlyphRunUtil.cs
new file mode 100644
index 0000000..7a17079
--- /dev/null
+++ b/Source/SVGImage/SVG/Utils/GlyphRunUtil.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Windows;
+using System.Windows.Media;
+
+namespace SVGImage.SVG.Utils
+{
+ internal static class GlyphRunUtil
+ {
+ public static GlyphRun CreateOffsetRun(this GlyphRun value, double xOffset, double yOffset)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+ return new GlyphRun(
+ value.GlyphTypeface,
+ value.BidiLevel,
+ value.IsSideways,
+ value.FontRenderingEmSize,
+#if DPI_AWARE
+ value.PixelsPerDip,
+#endif
+ value.GlyphIndices,
+ new Point( value.BaselineOrigin.X + xOffset, value.BaselineOrigin.Y + yOffset),
+ value.AdvanceWidths,
+ value.GlyphOffsets,
+ value.Characters,
+ value.DeviceFontName,
+ value.ClusterMap,
+ value.CaretStops,
+ value.Language);
+ }
+ }
+}
diff --git a/Source/SVGImage/SVG/Utils/MathUtils.cs b/Source/SVGImage/SVG/Utils/MathUtils.cs
new file mode 100644
index 0000000..3e47909
--- /dev/null
+++ b/Source/SVGImage/SVG/Utils/MathUtils.cs
@@ -0,0 +1,40 @@
+using SVGImage.SVG.Shapes;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Windows;
+using System.Windows.Media;
+
+namespace SVGImage.SVG.Utils
+{
+ internal static class MathUtil
+ {
+ public static bool IsNearlyZero(this double value, double epsilon = Double.Epsilon)
+ {
+ return Math.Abs(value) < epsilon;
+ }
+ public static bool IsNearlyEqual(this double a, double b, double epsilon = Double.Epsilon)
+ {
+ return Math.Abs(a - b) < epsilon;
+ }
+ public static int Clamp(int value, int min, int max)
+ {
+ if (min > max)
+ {
+ throw new ArgumentException("min must be less than or equal to max", nameof(min));
+ }
+
+ if (value < min)
+ {
+ return min;
+ }
+ else if (value > max)
+ {
+ return max;
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/Source/SVGImage/SVG/Utils/SubAndSuperScriptHelper.cs b/Source/SVGImage/SVG/Utils/SubAndSuperScriptHelper.cs
new file mode 100644
index 0000000..1735c13
--- /dev/null
+++ b/Source/SVGImage/SVG/Utils/SubAndSuperScriptHelper.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace SVGImage.SVG.Utils
+{
+ internal static class SubAndSuperScriptHelper
+ {
+ private static readonly Dictionary _subscriptMap = new Dictionary
+ {
+ {'0', '₀'}, {'1', '₁'}, {'2', '₂'}, {'3', '₃'}, {'4', '₄'},
+ {'5', '₅'}, {'6', '₆'}, {'7', '₇'}, {'8', '₈'}, {'9', '₉'},
+ {'+', '₊'}, {'-', '₋'}, {'=', '₌'}
+ };
+ private static readonly Dictionary _superscriptMap = new Dictionary
+ {
+ {'0', '⁰'}, {'1', '¹'}, {'2', '²'}, {'3', '³'}, {'4', '⁴'},
+ {'5', '⁵'}, {'6', '⁶'}, {'7', '⁷'}, {'8', '⁸'}, {'9', '⁹'},
+ {'+', '⁺'}, {'-', '⁻'}, {'=', '⁼'}
+ };
+ public static string ToSubscript(this string input)
+ {
+ return Convert(input, _subscriptMap);
+ }
+ public static string ToSuperscript(this string input)
+ {
+ return Convert(input, _superscriptMap);
+ }
+
+ private static string Convert(string input, Dictionary map)
+ {
+ var sb = new StringBuilder();
+ foreach (var c in input)
+ {
+ sb.Append(map.TryGetValue(c, out var mappedChar) ? mappedChar : c);
+ }
+ return sb.ToString();
+ }
+ }
+}