diff --git a/Source/SVGImage/DotNetProjects.SVGImage.csproj b/Source/SVGImage/DotNetProjects.SVGImage.csproj
index 69bd12b..02886db 100644
--- a/Source/SVGImage/DotNetProjects.SVGImage.csproj
+++ b/Source/SVGImage/DotNetProjects.SVGImage.csproj
@@ -52,12 +52,12 @@
$(DefineConstants);DOTNET40;NETFULL
$(DefineConstants);DOTNET45;NETFULL
$(DefineConstants);DOTNET46;NETFULL
- $(DefineConstants);DOTNET47;NETFULL
- $(DefineConstants);DOTNET48;NETFULL
- $(DefineConstants);NETCORE
- $(DefineConstants);NETCORE;NETNEXT
- $(DefineConstants);NETCORE;NETNEXT
- $(DefineConstants);NETCORE;NETNEXT
+ $(DefineConstants);DOTNET47;DPI_AWARE;NETFULL
+ $(DefineConstants);DOTNET48;DPI_AWARE;NETFULL
+ $(DefineConstants);NETCORE;DPI_AWARE
+ $(DefineConstants);NETCORE;DPI_AWARE;NETNEXT
+ $(DefineConstants);NETCORE;DPI_AWARE;NETNEXT
+ $(DefineConstants);NETCORE;DPI_AWARE;NETNEXT
True
SVGImage.snk
diff --git a/Source/SVGImage/SVG/SVGImage.cs b/Source/SVGImage/SVG/SVGImage.cs
index 26463c3..2f9c6a7 100644
--- a/Source/SVGImage/SVG/SVGImage.cs
+++ b/Source/SVGImage/SVG/SVGImage.cs
@@ -71,7 +71,7 @@ public enum eSizeType
DependencyProperty.Register("UriSource", typeof(Uri), typeof(SVGImage),
new FrameworkPropertyMetadata(null, OnUriSourceChanged));
- public static DependencyProperty SizeTypeProperty = DependencyProperty.Register("SizeType",
+ public static readonly DependencyProperty SizeTypeProperty = DependencyProperty.Register("SizeType",
typeof(eSizeType), typeof(SVGImage), new FrameworkPropertyMetadata(eSizeType.ContentToSizeNoStretch,
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(OnSizeTypeChanged)));
@@ -84,7 +84,7 @@ public enum eSizeType
public static readonly DependencyProperty FileSourceProperty = DependencyProperty.Register("FileSource",
typeof(string), typeof(SVGImage), new PropertyMetadata(OnFileSourceChanged));
- public static DependencyProperty ImageSourcePoperty = DependencyProperty.Register("ImageSource",
+ public static readonly DependencyProperty ImageSourcePoperty = DependencyProperty.Register("ImageSource",
typeof(Drawing), typeof(SVGImage), new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(OnImageSourceChanged)));
@@ -247,6 +247,11 @@ public Uri BaseUri
}
}
+
+
+ ///
+ /// Rerenders if CustomBrushesPropertyChanged, OverrideStrokeWidthPropertyChanged, OverrideStrokeColorPropertyChanged, OverrideFillColorPropertyChanged, OverrideColorPropertyChanged
+ ///
public void ReRenderSvg()
{
if (_render != null)
@@ -364,7 +369,17 @@ protected override void OnInitialized(EventArgs e)
_render.OverrideStrokeWidth = OverrideStrokeWidth;
_render.UseAnimations = this.UseAnimations;
- _loadImage(_render);
+ //This is to prevent double rendering because setting CustomBrushes = brushesFromSVG; invokes ReRenderSvg
+ //Not sure if it has any side effects
+ if (!String.IsNullOrEmpty(FileSource))
+ {
+ _render.LoadDrawingWithoutRender(FileSource);
+ }
+ else
+ {
+ _loadImage(_render);
+ }
+ //_loadImage(_render);
_loadImage = null;
var brushesFromSVG = new Dictionary();
foreach (var server in _render.SVG.PaintServers.GetServers())
diff --git a/Source/SVGImage/SVG/SVGRender.cs b/Source/SVGImage/SVG/SVGRender.cs
index 814d360..7039d93 100644
--- a/Source/SVGImage/SVG/SVGRender.cs
+++ b/Source/SVGImage/SVG/SVGRender.cs
@@ -65,6 +65,15 @@ public DrawingGroup LoadDrawing(string filename)
return this.CreateDrawing(this.SVG);
}
+ internal void LoadDrawingWithoutRender(string filename)
+ {
+ this.SVG = new SVG(filename, ExternalFileLoader);
+ }
+ internal void LoadDrawingWithoutRender(Stream stream)
+ {
+ this.SVG = new SVG(stream, ExternalFileLoader);
+ }
+
public DrawingGroup LoadXmlDrawing(string fileXml)
{
this.SVG = new SVG(this.ExternalFileLoader);
@@ -430,13 +439,13 @@ internal DrawingGroup LoadGroup(IList elements, Rect? viewBox, bool isSwi
}
if (shape is TextShape textShape)
{
- GeometryGroup gp = TextRender.BuildTextGeometry(textShape);
+ TextRender textRender2 = new TextRender();
+ GeometryGroup gp = textRender2.BuildTextGeometry(textShape);
if (gp != null)
{
foreach (Geometry gm in gp.Children)
{
- TextSpan tspan = TextRender.GetElement(gm);
- if (tspan != null)
+ if (TextRenderBase.GetElement(gm) is TextShapeBase tspan)
{
var di = this.NewDrawingItem(tspan, gm);
AddDrawingToGroup(grp, shape, di);
diff --git a/Source/SVGImage/SVG/Shapes/CharacterLayout.cs b/Source/SVGImage/SVG/Shapes/CharacterLayout.cs
new file mode 100644
index 0000000..9abd558
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/CharacterLayout.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace SVGImage.SVG.Shapes
+{
+ ///
+ /// Represents a per-character layout result.
+ ///
+ public class CharacterLayout
+ {
+ private CharacterLayout()
+ {
+ // Default constructor for array creation
+ }
+ public CharacterLayout(char character)
+ {
+ Character = character;
+ }
+ public char Character { get; set; } = '\0';
+ public int GlobalIndex { get; set; }
+ public double X { get; set; } = 0;
+ public double Y { get; set; } = 0;
+ public double DX { get; set; } = Double.NaN;
+ public double DY { get; set; } = Double.NaN;
+ public double Rotation { get; set; } = Double.NaN;
+ public bool Hidden { get; set; } = false;
+ public bool Addressable { get; set; } = true;
+ public bool Middle { get; set; } = false;
+ public bool AnchoredChunk { get; set; } = false;
+ ///
+ /// Not used, part of the SVG 2.0 spec.
+ ///
+ internal bool FirstCharacterInResolvedDescendant { get; set; }
+ ///
+ /// The character redefines the X position for anteceding characters.
+ ///
+ public bool DoesPositionX { get; internal set; }
+ ///
+ /// The character redefines the Y position for anteceding characters.
+ ///
+ public bool DoesPositionY { get; internal set; }
+
+
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/CircleShape.cs b/Source/SVGImage/SVG/Shapes/CircleShape.cs
index 91ba9c4..86e0167 100644
--- a/Source/SVGImage/SVG/Shapes/CircleShape.cs
+++ b/Source/SVGImage/SVG/Shapes/CircleShape.cs
@@ -8,15 +8,8 @@ namespace SVGImage.SVG.Shapes
public sealed class CircleShape : Shape
{
- private static Fill DefaultFill = null;
-
public CircleShape(SVG svg, XmlNode node) : base(svg, node)
{
- if (DefaultFill == null)
- {
- DefaultFill = Fill.CreateDefault(svg, "black");
- }
-
Rect? box = svg.ViewBox;
this.CX = XmlUtil.AttrValue(node, "cx", 0, box.HasValue ? box.Value.Width : svg.Size.Width);
@@ -27,14 +20,9 @@ public CircleShape(SVG svg, XmlNode node) : base(svg, node)
diagRef = Math.Sqrt(box.Value.Width * box.Value.Width + box.Value.Height * box.Value.Height) / Math.Sqrt(2);
this.R = XmlUtil.AttrValue(node, "r", 0, diagRef);
}
-
- public override Fill Fill
+ protected override Fill DefaultFill()
{
- get {
- Fill f = base.Fill;
- if (f == null) f = DefaultFill;
- return f;
- }
+ return Fill.CreateDefault(Svg, "black");
}
public double CX { get; set; }
diff --git a/Source/SVGImage/SVG/Shapes/EllipseShape.cs b/Source/SVGImage/SVG/Shapes/EllipseShape.cs
index b72a992..e07ea22 100644
--- a/Source/SVGImage/SVG/Shapes/EllipseShape.cs
+++ b/Source/SVGImage/SVG/Shapes/EllipseShape.cs
@@ -8,15 +8,9 @@ namespace SVGImage.SVG.Shapes
public sealed class EllipseShape : Shape
{
- private static Fill DefaultFill = null;
-
public EllipseShape(SVG svg, XmlNode node)
: base(svg, node)
- {
- if (DefaultFill == null)
- {
- DefaultFill = Fill.CreateDefault(svg, "black");
- }
+ {
Rect? box = svg.ViewBox;
@@ -30,13 +24,9 @@ public EllipseShape(SVG svg, XmlNode node)
this.RY = XmlUtil.AttrValue(node, "ry", 0, diagRef);
}
- public override Fill Fill
+ protected override Fill DefaultFill()
{
- get {
- Fill f = base.Fill;
- if (f == null) f = DefaultFill;
- return f;
- }
+ return Fill.CreateDefault(Svg, "black");
}
public double CX { get; set; }
diff --git a/Source/SVGImage/SVG/Shapes/Group.cs b/Source/SVGImage/SVG/Shapes/Group.cs
index f82986b..0eba3af 100644
--- a/Source/SVGImage/SVG/Shapes/Group.cs
+++ b/Source/SVGImage/SVG/Shapes/Group.cs
@@ -88,6 +88,7 @@ private Shape AddToList(SVG svg, XmlNode childnode, Shape parent, bool isDefinit
else if (nodeName == SVGTags.sAnimateTransform)
retVal = new AnimateTransform(svg, childnode, parent);
else if (nodeName == SVGTags.sText)
+ //retVal = new TextShape(svg, childnode, parent);
retVal = new TextShape(svg, childnode, parent);
else if (nodeName == SVGTags.sLinearGradient)
{
diff --git a/Source/SVGImage/SVG/Shapes/ITextChild.cs b/Source/SVGImage/SVG/Shapes/ITextChild.cs
new file mode 100644
index 0000000..80df697
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/ITextChild.cs
@@ -0,0 +1,11 @@
+namespace SVGImage.SVG.Shapes
+{
+ public interface ITextChild : ITextNode
+ {
+ Shape Parent { get; set; }
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/ITextNode.cs b/Source/SVGImage/SVG/Shapes/ITextNode.cs
new file mode 100644
index 0000000..c9d0da2
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/ITextNode.cs
@@ -0,0 +1,15 @@
+namespace SVGImage.SVG.Shapes
+{
+ public interface ITextNode
+ {
+ CharacterLayout GetFirstCharacter();
+ CharacterLayout GetLastCharacter();
+ string GetText();
+ int GetLength();
+ CharacterLayout[] GetCharacters();
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/LengthAdjustment.cs b/Source/SVGImage/SVG/Shapes/LengthAdjustment.cs
new file mode 100644
index 0000000..aef7fb8
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/LengthAdjustment.cs
@@ -0,0 +1,19 @@
+namespace SVGImage.SVG.Shapes
+{
+ public enum LengthAdjustment
+ {
+ None,
+ ///
+ /// Indicates that only the advance values are adjusted. The glyphs themselves are not stretched or compressed.
+ ///
+ Spacing,
+ ///
+ /// Indicates that the advance values are adjusted and the glyphs themselves stretched or compressed in one axis (i.e., a direction parallel to the inline-base direction).
+ ///
+ SpacingAndGlyphs
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/LengthContext.cs b/Source/SVGImage/SVG/Shapes/LengthContext.cs
new file mode 100644
index 0000000..e478af2
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/LengthContext.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+
+namespace SVGImage.SVG.Shapes
+{
+ public class LengthContext
+ {
+ public Shape Owner { get; set; }
+ public LengthUnit Unit { get; set; }
+ private static readonly Dictionary _unitMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ {"em", LengthUnit.em},
+ {"ex", LengthUnit.ex},
+ {"ch", LengthUnit.ch},
+ {"rem", LengthUnit.rem},
+ {"vw", LengthUnit.vw},
+ {"vh", LengthUnit.vh},
+ {"vmin", LengthUnit.vmin},
+ {"vmax", LengthUnit.vmax},
+ {"cm", LengthUnit.cm},
+ {"mm", LengthUnit.mm},
+ {"Q", LengthUnit.Q},
+ {"in", LengthUnit.Inches},
+ {"pc", LengthUnit.pc},
+ {"pt", LengthUnit.pt},
+ {"px", LengthUnit.px},
+ };
+
+ public LengthContext(Shape owner, LengthUnit unit)
+ {
+ Owner = owner;
+ Unit = unit;
+ }
+
+ public static LengthUnit Parse(string text, LengthOrientation orientation = LengthOrientation.None)
+ {
+ if (String.IsNullOrEmpty(text))
+ {
+ return LengthUnit.Number;
+ }
+ string trimmed = text.Trim();
+ if(trimmed == "%")
+ {
+ switch (orientation)
+ {
+ case LengthOrientation.Horizontal:
+ return LengthUnit.PercentWidth;
+ case LengthOrientation.Vertical:
+ return LengthUnit.PercentHeight;
+ default:
+ return LengthUnit.PercentDiagonal;
+ }
+ }
+ if(_unitMap.TryGetValue(trimmed, out LengthUnit unit))
+ {
+ return unit;
+ }
+ return LengthUnit.Unknown;
+ }
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/LengthOrientation.cs b/Source/SVGImage/SVG/Shapes/LengthOrientation.cs
new file mode 100644
index 0000000..351cbb2
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/LengthOrientation.cs
@@ -0,0 +1,13 @@
+namespace SVGImage.SVG.Shapes
+{
+ public enum LengthOrientation
+ {
+ None,
+ Horizontal,
+ Vertical,
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs
new file mode 100644
index 0000000..4b9ca9b
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs
@@ -0,0 +1,194 @@
+using System;
+
+namespace SVGImage.SVG.Shapes
+{
+ using System.Text.RegularExpressions;
+
+ public struct LengthPercentageOrNumber
+ {
+ private static readonly Regex _lengthRegex = new Regex(@"(?\d+(?:\.\d+)?)\s*(?%|\w+)?", RegexOptions.Compiled | RegexOptions.Singleline);
+ private readonly LengthContext _context;
+ private readonly double _value;
+ ///
+ /// Represents a length, percentage, or number value that has been resolved based on the context.
+ ///
+ public double Value => ResolveValue();
+
+
+ private static double ResolveAbsoluteValue(double value, LengthContext context)
+ {
+ switch (context.Unit)
+ {
+ case LengthUnit.cm:
+ return value * 35.43;
+ case LengthUnit.mm:
+ return value * 3.54;
+ case LengthUnit.Q:
+ return value * 3.54 / 4d;
+ case LengthUnit.Inches:
+ return value * 90d;
+ case LengthUnit.pc:
+ return value * 15d;
+ case LengthUnit.pt:
+ return value * 1.25;
+ case LengthUnit.px:
+ return value * 90d / 96d;
+ case LengthUnit.Unknown:
+ case LengthUnit.Number:
+ default:
+ return value;
+ }
+ }
+ private static double ResolveViewboxValue(double value, LengthContext context)
+ {
+ double height;
+ double width;
+ if (context.Owner.Svg.ViewBox.HasValue)
+ {
+ height = context.Owner.Svg.ViewBox.Value.Height;
+ width = context.Owner.Svg.ViewBox.Value.Width;
+ }
+ else
+ {
+ height = context.Owner.Svg.Size.Height;
+ width = context.Owner.Svg.Size.Width;
+ }
+ switch (context.Unit)
+ {
+ case LengthUnit.Percent:
+ throw new NotSupportedException($"Percent without specific orientation is not supported. Use ${LengthUnit.PercentWidth}, ${LengthUnit.PercentHeight}, or ${LengthUnit.PercentDiagonal} instead.");
+ case LengthUnit.PercentDiagonal:
+ return (value / 100d) * Math.Sqrt(Math.Pow(width, 2d) + Math.Pow(height, 2d));
+ case LengthUnit.vw:
+ case LengthUnit.PercentWidth:
+ return (value / 100d) * width;
+ case LengthUnit.vh:
+ case LengthUnit.PercentHeight:
+ return (value / 100d) * height;
+ case LengthUnit.vmin:
+ return (value / 100d) * Math.Min(width, height);
+ case LengthUnit.vmax:
+ return (value / 100d) * Math.Max(width, height);
+ case LengthUnit.Unknown:
+ case LengthUnit.Number:
+ default:
+ return value;
+ }
+ }
+ private static double ResolveRelativeValue(double value, LengthContext context)
+ {
+ switch (context.Unit)
+ {
+ case LengthUnit.em:
+ return value * context.Owner.TextStyle.FontSize;
+ case LengthUnit.ex:
+ return value * context.Owner.TextStyle.GetTypeface().XHeight;
+ case LengthUnit.ch:
+ var glyphTypeface = context.Owner.TextStyle.GetGlyphTypeface();
+ return value * glyphTypeface.AdvanceWidths[glyphTypeface.CharacterToGlyphMap['0']];
+ case LengthUnit.rem:
+ return value * context.Owner.GetRoot().TextStyle.FontSize;
+ case LengthUnit.Unknown:
+ case LengthUnit.Number:
+ default:
+ return value;
+ }
+ }
+ private double ResolveValue()
+ {
+ if (_context == null)
+ {
+ return _value; // No context, return raw value
+
+ }
+
+ switch (_context.Unit)
+ {
+ case LengthUnit.Percent:
+ case LengthUnit.PercentWidth:
+ case LengthUnit.PercentHeight:
+ case LengthUnit.PercentDiagonal:
+ case LengthUnit.vw:
+ case LengthUnit.vh:
+ case LengthUnit.vmin:
+ case LengthUnit.vmax:
+ return ResolveViewboxValue(_value, _context);
+ case LengthUnit.em:
+ case LengthUnit.ex:
+ case LengthUnit.ch:
+ case LengthUnit.rem:
+ return ResolveRelativeValue(_value, _context);
+ case LengthUnit.cm:
+ case LengthUnit.mm:
+ case LengthUnit.Q:
+ case LengthUnit.Inches:
+ case LengthUnit.pc:
+ case LengthUnit.pt:
+ case LengthUnit.px:
+ return ResolveAbsoluteValue(_value, _context);
+ case LengthUnit.Unknown:
+ case LengthUnit.Number:
+ default:
+ return _value;
+ }
+ }
+ ///
+ /// Represents a length that may have a value that is context dependent.
+ ///
+ /// The numerical part of the length that is related to the
+ /// If , units will be ignored
+ public LengthPercentageOrNumber(double value, LengthContext context)
+ {
+ _context = context;
+ _value = value;
+ }
+ ///
+ /// Parses a string representation of a length, percentage, or number value into a instance.
+ ///
+ ///
+ /// The element that the length is associated with.
+ ///
+ /// A string representation of a length
+ /// Used to establish the context of the length.
+ /// Should be for inherntly horizontal values like 'x' and 'dx'.
+ /// Should be for inherntly vertical values like 'y' and 'dy'.
+ /// Should be for other values.
+ ///
+ ///
+ /// A instance that represents the parsed value.
+ ///
+ ///
+ /// Thrown when the provided value is not a valid length, percentage, or number.
+ ///
+ public static LengthPercentageOrNumber Parse(Shape owner, string value, LengthOrientation orientation = LengthOrientation.None)
+ {
+ var lengthMatch = _lengthRegex.Match(value.Trim());
+ if(!lengthMatch.Success || !Double.TryParse(lengthMatch.Groups["Value"].Value, out double d))
+ {
+ throw new ArgumentException($"Invalid length/percentage/number value: {value}");
+ }
+ LengthContext context;
+ if (lengthMatch.Groups["Unit"].Success)
+ {
+ string unitStr = lengthMatch.Groups["Unit"].Value;
+ LengthUnit unit = LengthContext.Parse(unitStr, orientation);
+ if (unit == LengthUnit.Unknown)
+ {
+ throw new ArgumentException($"Unknown length unit: {unitStr}");
+ }
+ context = new LengthContext(owner, unit);
+ }
+ else
+ {
+ // Default to pixels if no unit is specified
+ context = new LengthContext(owner, LengthUnit.px);
+ }
+ return new LengthPercentageOrNumber(d, context);
+ }
+
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs
new file mode 100644
index 0000000..0bddebf
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+
+namespace SVGImage.SVG.Shapes
+{
+ using System.Linq;
+ using System.Text.RegularExpressions;
+ using System.Collections;
+
+ public class LengthPercentageOrNumberList : IList
+ {
+ private readonly Shape _owner;
+ private List _list = new List();
+ private readonly LengthOrientation _orientation;
+ private static readonly Regex _splitRegex = new Regex(@"\b(?:,|\s*,?\s+)\b", RegexOptions.Compiled);
+ private LengthPercentageOrNumberList(Shape owner, LengthOrientation orientation = LengthOrientation.None)
+ {
+ _owner = owner;
+ _orientation = orientation;
+ }
+ ///
+ /// Creates a new instance of LengthPercentageOrNumberList with the specified owner and orientation.
+ ///
+ ///
+ /// The element that the length list is associated with.
+ ///
+ /// A string representation of a length list
+ /// Used to establish the context of the lengths.
+ /// Should be for inherntly horizontal values like 'x' and 'dx'.
+ /// Should be for inherntly vertical values like 'y' and 'dy'.
+ /// Should be for other values.
+ ///
+ public LengthPercentageOrNumberList(Shape owner, string value, LengthOrientation orientation = LengthOrientation.None) : this(owner, orientation)
+ {
+ Parse(value);
+ }
+ private void Parse(string value)
+ {
+ string[] list = _splitRegex.Split(value.Trim());
+
+ if (list.Any(string.IsNullOrEmpty))
+ {
+ throw new ArgumentException("Invalid length/percentage/number list: " + value);
+ }
+ _list = list.Select(s=>LengthPercentageOrNumber.Parse(_owner, s, _orientation)).ToList();
+ }
+
+ public static LengthPercentageOrNumberList Empty(Shape owner, LengthOrientation orientation = LengthOrientation.None)
+ {
+ return new LengthPercentageOrNumberList(owner, orientation);
+ }
+
+
+ public LengthPercentageOrNumber this[int index] { get => ((IList)_list)[index]; set => ((IList)_list)[index] = value; }
+
+ public int Count => ((ICollection)_list).Count;
+
+ public bool IsReadOnly => ((ICollection)_list).IsReadOnly;
+
+ public void Add(LengthPercentageOrNumber item)
+ {
+ //Remove units because Child elements do not inherit the relative values as specified for their parent; they inherit the computed values.
+ var strippedContext = new LengthPercentageOrNumber(item.Value, new LengthContext(_owner, LengthUnit.Number));
+ ((ICollection)_list).Add(strippedContext);
+ }
+
+ public void Clear()
+ {
+ ((ICollection)_list).Clear();
+ }
+
+ public bool Contains(LengthPercentageOrNumber item)
+ {
+ return ((ICollection)_list).Contains(item);
+ }
+
+ public void CopyTo(LengthPercentageOrNumber[] array, int arrayIndex)
+ {
+ ((ICollection)_list).CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return ((IEnumerable)_list).GetEnumerator();
+ }
+
+ public int IndexOf(LengthPercentageOrNumber item)
+ {
+ return ((IList)_list).IndexOf(item);
+ }
+
+ public void Insert(int index, LengthPercentageOrNumber item)
+ {
+ //Remove units because Child elements do not inherit the relative values as specified for their parent; they inherit the computed values.
+ var strippedContext = new LengthPercentageOrNumber(item.Value, new LengthContext(_owner, LengthUnit.Number));
+ ((ICollection)_list).Add(strippedContext);
+ ((IList)_list).Insert(index, strippedContext);
+ }
+
+ public bool Remove(LengthPercentageOrNumber item)
+ {
+ return ((ICollection)_list).Remove(item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ ((IList)_list).RemoveAt(index);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)_list).GetEnumerator();
+ }
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/LengthUnit.cs b/Source/SVGImage/SVG/Shapes/LengthUnit.cs
new file mode 100644
index 0000000..c1e1c7b
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/LengthUnit.cs
@@ -0,0 +1,123 @@
+namespace SVGImage.SVG.Shapes
+{
+ ///
+ ///
+ ///
+ ///
+ /// Child elements do not inherit the relative values as specified for their parent; they inherit the computed values.
+ /// Values from to are 1 less than as defined in the SVG 1.1 SVG_LENGTHTYPE specification. This is so can be used as the default.
+ ///
+ public enum LengthUnit
+ {
+ ///
+ /// A unit was detected but not recgonized.
+ ///
+ ///
+ Unknown = -1,
+ ///
+ /// No unit specified, interpreted as a value in pixels.
+ ///
+ ///
+ Number,
+
+ ///
+ /// Percent is relative to least distant viewbox dimensions.
+ /// If the length is inherently horizontal, like "dx", then the percentage is relative to the least distant viewbox width.
+ /// If the length is inherently vertical, like "dy", then the percentage is relative to the least distant viewbox height.
+ /// Otherwise the percentage is relative to the least distant viewbox diagonal.
+ ///
+ ///
+ /// Setting a unit of may be converted into , , or
+ ///
+ Percent,
+
+ ///
+ /// Relative to font size of the element
+ ///
+ em,
+
+ ///
+ /// Relative to x-height of the element’s font
+ ///
+ ex,
+
+ ///
+ /// pixels 1px = 1/96th of 1in
+ ///
+ px,
+ ///
+ /// centimeters 1cm = 96px/2.54
+ ///
+ cm,
+
+ ///
+ /// millimeters 1mm = 1/10th of 1cm
+ ///
+ mm,
+
+ ///
+ /// inches 1in = 2.54cm = 96px
+ ///
+ Inches,
+
+ ///
+ /// points 1pt = 1/72nd of 1in
+ ///
+ pt,
+
+ ///
+ /// picas 1pc = 1/6th of 1in
+ ///
+ pc,
+
+ ///
+ /// quarter-millimeters 1Q = 1/40th of 1cm
+ ///
+ Q,
+
+ ///
+ /// Relative to character advance of the “0” (ZERO, U+0030) glyph in the element’s font
+ ///
+ ch,
+
+ ///
+ /// Relative to font size of the root element
+ ///
+ rem,
+
+ ///
+ /// Relative to 1% of viewport’s width
+ ///
+ vw,
+
+ ///
+ /// Relative to 1% of viewport’s height
+ ///
+ vh,
+
+ ///
+ /// Relative to 1% of viewport’s smaller dimension
+ ///
+ vmin,
+
+ ///
+ /// Relative to 1% of viewport’s larger dimension
+ ///
+ vmax,
+
+ ///
+ /// Percentage is relative to the least distant viewbox width.
+ ///
+ PercentWidth,
+
+ ///
+ /// Percentage is relative to the least distant viewbox height.
+ ///
+ PercentHeight,
+
+ ///
+ /// Percentage is relative to the least distant viewbox diagonal
+ ///
+ PercentDiagonal,
+ }
+}
diff --git a/Source/SVGImage/SVG/Shapes/PathShape.cs b/Source/SVGImage/SVG/Shapes/PathShape.cs
index 33ca7c6..35b58e5 100644
--- a/Source/SVGImage/SVG/Shapes/PathShape.cs
+++ b/Source/SVGImage/SVG/Shapes/PathShape.cs
@@ -7,30 +7,17 @@ namespace SVGImage.SVG
public sealed class PathShape : Shape
{
- static Fill DefaultFill = null;
// http://apike.ca/prog_svg_paths.html
public PathShape(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent)
{
- if (DefaultFill == null)
- {
- DefaultFill = Fill.CreateDefault(svg, "black");
- }
-
this.ClosePath = false;
string path = XmlUtil.AttrValue(node, "d", string.Empty);
this.Data = path;
}
-
- public override Fill Fill
+ protected override Fill DefaultFill()
{
- get
- {
- Fill f = base.Fill;
- if (f == null)
- f = DefaultFill;
- return f;
- }
+ return Fill.CreateDefault(Svg, "black");
}
public bool ClosePath { get; private set; }
diff --git a/Source/SVGImage/SVG/Shapes/PolygonShape.cs b/Source/SVGImage/SVG/Shapes/PolygonShape.cs
index 0e82bb5..777924f 100644
--- a/Source/SVGImage/SVG/Shapes/PolygonShape.cs
+++ b/Source/SVGImage/SVG/Shapes/PolygonShape.cs
@@ -8,15 +8,9 @@ namespace SVGImage.SVG.Shapes
public sealed class PolygonShape : Shape
{
- private static Fill DefaultFill = null;
-
public PolygonShape(SVG svg, XmlNode node)
: base(svg, node)
{
- if (DefaultFill == null)
- {
- DefaultFill = Fill.CreateDefault(svg, "black");
- }
string points = XmlUtil.AttrValue(node, SVGTags.sPoints, string.Empty);
var split = new StringSplitter(points);
@@ -30,14 +24,9 @@ public PolygonShape(SVG svg, XmlNode node)
public Point[] Points { get; private set; }
- public override Fill Fill
+ protected override Fill DefaultFill()
{
- get
- {
- Fill f = base.Fill;
- if (f == null) f = DefaultFill;
- return f;
- }
+ return Fill.CreateDefault(Svg, "black");
}
}
}
diff --git a/Source/SVGImage/SVG/Shapes/RectangleShape.cs b/Source/SVGImage/SVG/Shapes/RectangleShape.cs
index 2144443..a360fe3 100644
--- a/Source/SVGImage/SVG/Shapes/RectangleShape.cs
+++ b/Source/SVGImage/SVG/Shapes/RectangleShape.cs
@@ -6,24 +6,14 @@ namespace SVGImage.SVG.Shapes
public sealed class RectangleShape : Shape
{
- private static Fill DefaultFill = null;
public RectangleShape(SVG svg, XmlNode node) : base(svg, node)
{
- if (DefaultFill == null)
- {
- DefaultFill = Fill.CreateDefault(svg, "black");
- }
}
- public override Fill Fill
+ protected override Fill DefaultFill()
{
- get
- {
- Fill f = base.Fill;
- if (f == null) f = DefaultFill;
- return f;
- }
+ return Fill.CreateDefault(Svg, "black");
}
public double X { get; set; }
diff --git a/Source/SVGImage/SVG/Shapes/Shape.cs b/Source/SVGImage/SVG/Shapes/Shape.cs
index 9bc707f..7d45c29 100644
--- a/Source/SVGImage/SVG/Shapes/Shape.cs
+++ b/Source/SVGImage/SVG/Shapes/Shape.cs
@@ -7,10 +7,19 @@
namespace SVGImage.SVG.Shapes
{
using Utils;
- using Filters;
-
+ using Filters;
+ using System.Linq;
+ using System.Text.RegularExpressions;
+ using System.Windows.Shapes;
+ using System.Windows.Markup;
+ using System.Diagnostics;
+ using System.Text;
+ using System.Collections;
+ using System.Reflection;
+
public class Shape : ClipArtElement
{
+ private static readonly Regex _whiteSpaceRegex = new Regex(@"\s+");
protected Fill m_fill;
protected Stroke m_stroke;
@@ -22,8 +31,10 @@ public class Shape : ClipArtElement
internal Clip m_clip = null;
internal Geometry geometryElement;
+ private readonly SVG _svg;
+ public SVG Svg => _svg;
- public bool Display = true;
+ public bool Display { get; private set; } = true;
//Used during render
internal Shape RealParent;
@@ -33,27 +44,45 @@ public Shape(SVG svg, XmlNode node) : this(svg, node, null)
{
}
+ public Shape GetRoot()
+ {
+ Shape root = this;
+ while (root.Parent != null)
+ {
+ root = root.Parent;
+ }
+ return root;
+ }
+
+
public Shape(SVG svg, XmlNode node, Shape parent) : base(node)
{
+ _svg = svg;
this.Opacity = 1;
this.Parent = parent;
this.ParseAtStart(svg, node);
if (node != null)
{
- foreach (XmlAttribute attr in node.Attributes)
- this.Parse(svg, attr.Name, attr.Value);
+ _ = GetTextStyle(svg); // Ensure TextStyle is initialized
+ foreach (XmlAttribute attr in node.Attributes)
+ {
+ this.Parse(svg, attr.Name, attr.Value);
+ }
}
ParseLocalStyle(svg);
}
public Shape(SVG svg, List attrs, Shape parent) : base(null)
{
+ _svg = svg;
this.Opacity = 1;
this.Parent = parent;
if (attrs != null)
{
- foreach (StyleItem attr in attrs)
- this.Parse(svg, attr.Name, attr.Value);
+ foreach (StyleItem attr in attrs)
+ {
+ this.Parse(svg, attr.Name, attr.Value);
+ }
}
ParseLocalStyle(svg);
}
@@ -74,53 +103,65 @@ internal Clip Clip
public Visibility Visibility { get; set; }
- public virtual Stroke Stroke
- {
- get
- {
- if (this.m_stroke != null) return this.m_stroke;
- var parent = this.Parent;
- while (parent != null)
- {
- if (this.Parent.Stroke != null) return parent.Stroke;
- parent = parent.Parent;
- }
- return null;
- }
- set
- {
- m_stroke = value;
- }
+ protected virtual Stroke DefaultStroke()
+ {
+ var parent = this.Parent;
+ while (parent != null)
+ {
+ if (parent.Stroke != null)
+ {
+ return parent.Stroke;
+ }
+
+ parent = parent.Parent;
+ }
+ return null;
+ }
+
+ public Stroke Stroke
+ {
+ get => m_stroke ?? DefaultStroke();
+ set => m_stroke = value;
}
- public virtual Fill Fill
- {
- get
- {
- if (this.m_fill != null) return this.m_fill;
- var parent = this.Parent;
- while (parent != null)
- {
- if (parent.Fill != null) return parent.Fill;
- parent = parent.Parent;
- }
- return null;
- }
- set
- {
- m_fill = value;
- }
+ protected virtual Fill DefaultFill()
+ {
+ var parent = this.Parent;
+ while (parent != null)
+ {
+ if (parent.Fill != null)
+ {
+ return parent.Fill;
+ }
+
+ parent = parent.Parent;
+ }
+ return null;
+ }
+
+ public Fill Fill
+ {
+ get => m_fill ?? DefaultFill();
+ set => m_fill = value;
}
public virtual TextStyle TextStyle
{
get
{
- if (this.m_textstyle != null) return this.m_textstyle;
+ if (this.m_textstyle != null)
+ {
+ return this.m_textstyle;
+ }
+
var parent = this.Parent;
while (parent != null)
{
- if (parent.m_textstyle != null) return parent.m_textstyle;
+ if (parent.m_textstyle != null)
+ {
+ return parent.m_textstyle;
+ }
+
parent = parent.Parent;
}
return null;
@@ -144,9 +185,11 @@ protected virtual void ParseAtStart(SVG svg, XmlNode node)
if (node != null)
{
var name = node.Name;
- if (name.Contains(":"))
- name = name.Split(':')[1];
-
+ if (name.Contains(':'))
+ {
+ name = name.Split(':')[1];
+ }
+
if (svg.m_styles.TryGetValue(name, out var attributes))
{
foreach (var xmlAttribute in attributes)
@@ -155,14 +198,11 @@ protected virtual void ParseAtStart(SVG svg, XmlNode node)
}
}
- if (!string.IsNullOrEmpty(this.Id))
- {
- if (svg.m_styles.TryGetValue("#" + this.Id, out attributes))
- {
- foreach (var xmlAttribute in attributes)
- {
- Parse(svg, xmlAttribute.Name, xmlAttribute.Value);
- }
+ if (!string.IsNullOrEmpty(this.Id) && svg.m_styles.TryGetValue("#" + this.Id, out attributes))
+ {
+ foreach (var xmlAttribute in attributes)
+ {
+ Parse(svg, xmlAttribute.Name, xmlAttribute.Value);
}
}
}
@@ -170,16 +210,22 @@ protected virtual void ParseAtStart(SVG svg, XmlNode node)
protected virtual void ParseLocalStyle(SVG svg)
{
- if (!string.IsNullOrEmpty(this.m_localStyle))
- foreach (StyleItem item in StyleParser.SplitStyle(svg, this.m_localStyle))
- this.Parse(svg, item.Name, item.Value);
+ if (!string.IsNullOrEmpty(this.m_localStyle))
+ {
+ foreach (StyleItem item in StyleParser.SplitStyle(svg, this.m_localStyle))
+ {
+ this.Parse(svg, item.Name, item.Value);
+ }
+ }
}
protected virtual void Parse(SVG svg, string name, string value)
{
- if (name.Contains(":"))
- name = name.Split(':')[1];
-
+ if (name.Contains(':'))
+ {
+ name = name.Split(':')[1];
+ }
+
if (name == SVGTags.sDisplay && value == "none")
{
this.Display = false;
@@ -253,13 +299,19 @@ protected virtual void Parse(SVG svg, string name, string value)
if (name == SVGTags.sStrokeLinecap)
{
if (Enum.TryParse(value, true, out var parsed))
- this.GetStroke(svg).LineCap = parsed;
+ {
+ this.GetStroke(svg).LineCap = parsed;
+ }
+
return;
}
if (name == SVGTags.sStrokeLinejoin)
{
if (Enum.TryParse(value, true, out var parsed))
- this.GetStroke(svg).LineJoin = parsed;
+ {
+ this.GetStroke(svg).LineJoin = parsed;
+ }
+
return;
}
if (name == SVGTags.sFilterProperty)
@@ -268,7 +320,11 @@ protected virtual void Parse(SVG svg, string name, string value)
{
Shape result;
string id = ShapeUtil.ExtractBetween(value, '(', ')');
- if (id.Length > 0 && id[0] == '#') id = id.Substring(1);
+ if (id.Length > 0 && id[0] == '#')
+ {
+ id = id.Substring(1);
+ }
+
svg.m_shapes.TryGetValue(id, out result);
this.Filter = result as Filter;
return;
@@ -281,7 +337,11 @@ protected virtual void Parse(SVG svg, string name, string value)
{
Shape result;
string id = ShapeUtil.ExtractBetween(value, '(', ')');
- if (id.Length > 0 && id[0] == '#') id = id.Substring(1);
+ if (id.Length > 0 && id[0] == '#')
+ {
+ id = id.Substring(1);
+ }
+
svg.m_shapes.TryGetValue(id, out result);
this.m_clip = result as Clip;
return;
@@ -306,7 +366,10 @@ protected virtual void Parse(SVG svg, string name, string value)
if (name == SVGTags.sFillRule)
{
if (Enum.TryParse(value, true, out var parsed))
- this.GetFill(svg).FillRule = parsed;
+ {
+ this.GetFill(svg).FillRule = parsed;
+ }
+
return;
}
if (name == SVGTags.sOpacity)
@@ -341,21 +404,58 @@ protected virtual void Parse(SVG svg, string name, string value)
}
if (name == SVGTags.sTextDecoration)
{
- TextDecoration t = new TextDecoration();
- if (value == "none") return;
- if (value == "underline") t.Location = TextDecorationLocation.Underline;
- if (value == "overline") t.Location = TextDecorationLocation.OverLine;
- if (value == "line-through") t.Location = TextDecorationLocation.Strikethrough;
- TextDecorationCollection tt = new TextDecorationCollection();
- tt.Add(t);
- this.GetTextStyle(svg).TextDecoration = tt;
+ if (value == "none")
+ {
+ return;
+ }
+ var textDecorations = _whiteSpaceRegex.Split(value);
+ TextDecorationCollection tt = new TextDecorationCollection();
+ if (textDecorations.Length == 0 || textDecorations.Any(td => td.Equals("none", StringComparison.OrdinalIgnoreCase)))
+ {
+ // If "none" is explicitly set, set TextDecoration to empty collection.
+ // This distinguishes it from not setting TextDecoration at all, in which case it defaults to inherit.
+ this.GetTextStyle(svg).TextDecoration = tt;
+ return;
+ }
+ foreach (var textDecoration in textDecorations)
+ {
+ TextDecoration t = new TextDecoration();
+ if (value == "underline")
+ {
+ t.Location = TextDecorationLocation.Underline;
+ }
+ else if (value == "overline")
+ {
+ t.Location = TextDecorationLocation.OverLine;
+ }
+ else if (value == "line-through")
+ {
+ t.Location = TextDecorationLocation.Strikethrough;
+ }
+ tt.Add(t);
+ }
+
+ this.GetTextStyle(svg).TextDecoration = tt;
+
return;
}
if (name == SVGTags.sTextAnchor)
{
- if (value == "start") this.GetTextStyle(svg).TextAlignment = TextAlignment.Left;
- if (value == "middle") this.GetTextStyle(svg).TextAlignment = TextAlignment.Center;
- if (value == "end") this.GetTextStyle(svg).TextAlignment = TextAlignment.Right;
+ if (value == "start")
+ {
+ this.GetTextStyle(svg).TextAlignment = TextAlignment.Left;
+ }
+
+ if (value == "middle")
+ {
+ this.GetTextStyle(svg).TextAlignment = TextAlignment.Center;
+ }
+
+ if (value == "end")
+ {
+ this.GetTextStyle(svg).TextAlignment = TextAlignment.Right;
+ }
+
return;
}
if (name == "word-spacing")
@@ -380,19 +480,31 @@ protected virtual void Parse(SVG svg, string name, string value)
private Stroke GetStroke(SVG svg)
{
- if (this.m_stroke == null) this.m_stroke = new Stroke(svg);
+ if (this.m_stroke == null)
+ {
+ this.m_stroke = new Stroke(svg);
+ }
+
return this.m_stroke;
}
protected Fill GetFill(SVG svg)
{
- if (this.m_fill == null) this.m_fill = new Fill(svg);
+ if (this.m_fill == null)
+ {
+ this.m_fill = new Fill(svg);
+ }
+
return this.m_fill;
}
protected TextStyle GetTextStyle(SVG svg)
{
- if (this.m_textstyle == null) this.m_textstyle = new TextStyle(svg, this);
+ if (this.m_textstyle == null)
+ {
+ this.m_textstyle = new TextStyle( this);
+ }
+
return this.m_textstyle;
}
@@ -400,5 +512,9 @@ public override string ToString()
{
return this.GetType().Name + " (" + (Id ?? "") + ")";
}
- }
+ }
+
+
+
+
}
diff --git a/Source/SVGImage/SVG/Shapes/Text.cs b/Source/SVGImage/SVG/Shapes/Text.cs
index 1a3a7c8..4ba2e0a 100644
--- a/Source/SVGImage/SVG/Shapes/Text.cs
+++ b/Source/SVGImage/SVG/Shapes/Text.cs
@@ -6,13 +6,14 @@ namespace SVGImage.SVG
{
using Utils;
using Shapes;
+ using System.Linq;
- public sealed class TextShape : Shape
+ ///
+ /// The original TextShape class.
+ ///
+ public sealed class TextShape2 : Shape
{
- private static Fill DefaultFill = null;
- private static Stroke DefaultStroke = null;
-
- public TextShape(SVG svg, XmlNode node, Shape parent)
+ public TextShape2(SVG svg, XmlNode node, Shape parent)
: base(svg, node, parent)
{
this.X = XmlUtil.AttrValue(node, "x", 0);
@@ -22,48 +23,27 @@ public TextShape(SVG svg, XmlNode node, Shape parent)
// check for tSpan tag
if (node.InnerXml.IndexOf("<") >= 0)
this.TextSpan = this.ParseTSpan(svg, node.InnerXml);
- if (DefaultFill == null)
- {
- DefaultFill = Fill.CreateDefault(svg, "black");
- }
- if (DefaultStroke == null)
- {
- DefaultStroke = Stroke.CreateDefault(svg, 0.1);
- }
}
public double X { get; set; }
public double Y { get; set; }
public string Text { get; set; }
- public TextSpan TextSpan {get; private set;}
+ public TextSpan2 TextSpan { get; private set; }
- public override Fill Fill
- {
- get
- {
- Fill f = base.Fill;
- if (f == null)
- f = DefaultFill;
- return f;
- }
+ protected override Fill DefaultFill()
+ {
+ return Fill.CreateDefault(Svg, "black");
}
-
- public override Stroke Stroke
- {
- get
- {
- Stroke f = base.Stroke;
- if (f == null)
- f = DefaultStroke;
- return f;
- }
+ protected override Stroke DefaultStroke()
+ {
+ return Stroke.CreateDefault(Svg, 0.1);
}
- TextSpan ParseTSpan(SVG svg, string tspanText)
+ TextSpan2 ParseTSpan(SVG svg, string tspanText)
{
try
{
- return TextSpan.Parse(svg, tspanText, this);
+ return TextSpan2.Parse(svg, tspanText, this);
}
catch
{
@@ -71,8 +51,7 @@ TextSpan ParseTSpan(SVG svg, string tspanText)
}
}
}
-
- public class TextSpan : Shape
+ public class TextSpan2 : Shape
{
public enum eElementType
{
@@ -80,39 +59,39 @@ public enum eElementType
Text,
}
- public override System.Windows.Media.Transform Transform
- {
- get {return this.Parent.Transform; }
+ public override System.Windows.Media.Transform Transform
+ {
+ get { return this.Parent.Transform; }
}
- public eElementType ElementType {get; private set;}
- public List Attributes {get; set;}
- public List Children {get; private set;}
- public int StartIndex {get; set;}
- public string Text {get; set;}
- public TextSpan End {get; set;}
- public TextSpan(SVG svg, Shape parent, string text) : base(svg, (XmlNode)null, parent)
+ public eElementType ElementType { get; private set; }
+ public List Attributes { get; set; }
+ public List Children { get; private set; }
+ public int StartIndex { get; set; }
+ public string Text { get; set; }
+ public TextSpan2 End { get; set; }
+ public TextSpan2(SVG svg, Shape parent, string text) : base(svg, (XmlNode)null, parent)
{
this.ElementType = eElementType.Text;
this.Text = text;
}
- public TextSpan(SVG svg, Shape parent, eElementType eType, List attrs)
+ public TextSpan2(SVG svg, Shape parent, eElementType eType, List attrs)
: base(svg, attrs, parent)
{
this.ElementType = eType;
this.Text = string.Empty;
- this.Children = new List();
+ this.Children = new List();
}
public override string ToString()
{
return this.Text;
}
- static TextSpan NextTag(SVG svg, TextSpan parent, string text, ref int curPos)
+ static TextSpan2 NextTag(SVG svg, TextSpan2 parent, string text, ref int curPos)
{
int start = text.IndexOf("<", curPos);
if (start < 0)
return null;
- int end = text.IndexOf(">", start+1);
+ int end = text.IndexOf(">", start + 1);
if (end < 0)
throw new Exception("Start '<' with no end '>'");
@@ -127,11 +106,11 @@ static TextSpan NextTag(SVG svg, TextSpan parent, string text, ref int curPos)
if (attrstart > 0)
{
attrstart += 5;
- while (attrstart < tagtext.Length-1)
+ while (attrstart < tagtext.Length - 1)
attrs.Add(StyleItem.ReadNextAttr(tagtext, ref attrstart));
}
-
- TextSpan tag = new TextSpan(svg, parent, eElementType.Tag, attrs);
+
+ TextSpan2 tag = new TextSpan2(svg, parent, eElementType.Tag, attrs);
tag.StartIndex = start;
tag.Text = text.Substring(start, end - start);
if (tag.Text.IndexOf("<", 1) > 0)
@@ -141,28 +120,28 @@ static TextSpan NextTag(SVG svg, TextSpan parent, string text, ref int curPos)
return tag;
}
- static TextSpan Parse(SVG svg, string text, ref int curPos, TextSpan parent, TextSpan curTag)
+ static TextSpan2 Parse(SVG svg, string text, ref int curPos, TextSpan2 parent, TextSpan2 curTag)
{
- TextSpan tag = curTag;
+ TextSpan2 tag = curTag;
if (tag == null)
tag = NextTag(svg, parent, text, ref curPos);
while (curPos < text.Length)
{
int prevPos = curPos;
- TextSpan next = NextTag(svg, tag, text, ref curPos);
+ TextSpan2 next = NextTag(svg, tag, text, ref curPos);
if (next == null && curPos < text.Length)
{
// remaining pure text
string s = text.Substring(curPos, text.Length - curPos);
- tag.Children.Add(new TextSpan(svg, tag, s));
+ tag.Children.Add(new TextSpan2(svg, tag, s));
return tag;
}
- if (next != null && next.StartIndex-prevPos > 0)
+ if (next != null && next.StartIndex - prevPos > 0)
{
// pure text between tspan elements
- int diff = next.StartIndex-prevPos;
+ int diff = next.StartIndex - prevPos;
string s = text.Substring(prevPos, diff);
- tag.Children.Add(new TextSpan(svg, tag, s));
+ tag.Children.Add(new TextSpan2(svg, tag, s));
}
if (next.Text.StartsWith("
+ /// This is an unfinished scaffold for resolving the 'textLength' attribute in SVG text elements.
+ /// I was using the SVG 2.0 specification as a reference, but it is not complete, and it is confusing.
+ ///
+ internal class TextLengthResolver
+ {
+ private readonly CharacterLayout[] _result;
+ private readonly bool _horizontal;
+ private readonly double[] _resolveDx;
+ private readonly double[] _resolveDy;
+
+ public TextLengthResolver(CharacterLayout[] result, bool horizontal, double[] resolveDx, double[] resolveDy)
+ {
+ _result = result;
+ _horizontal = horizontal;
+ _resolveDx = resolveDx;
+ _resolveDy = resolveDy;
+ }
+
+
+ ///
+ /// Define resolved descendant node as a descendant of node with a valid ‘textLength’ attribute that is not itself a descendant node of a descendant node that has a valid ‘textLength’ attribute
+ ///
+ ///
+ ///
+ ///
+ private bool IsResolvedDescendantNode(ITextNode textNode, ITextNode descendant)
+ {
+ if (textNode is TextShapeBase textShape)
+ {
+ if (textShape.TextLength != null && !textShape.Children.Any(c => c is TextShapeBase child && child.TextLength != null))
+ {
+ return true;
+ }
+ foreach (var child in textShape.Children)
+ {
+ if (IsResolvedDescendantNode(child, descendant))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ public void ResolveTextLength(ITextNode textNode, string text, ref bool in_text_path)
+ {
+ if (textNode is TextShapeBase textNodeBase)
+ {
+ foreach (var child in textNodeBase.Children)
+ {
+ ResolveTextLength(child, text, ref in_text_path);
+ }
+ }
+ if (textNode is TextSpan textSpan && textSpan.TextLength != null && !IsResolvedDescendantNode(textNode, textSpan))
+ {
+ // Calculate total advance width
+ double totalAdvance = 0;
+ for (int i = 0; i < _result.Length; i++)
+ {
+ if (_result[i].Addressable)
+ {
+ totalAdvance += _resolveDx[i];
+ }
+ }
+ // Calculate scaling factor
+ double scaleFactor = textSpan.TextLength.Value.Value / totalAdvance;
+ // Apply scaling to dx and dy
+ for (int i = 0; i < _result.Length; i++)
+ {
+ if (_result[i].Addressable)
+ {
+ _resolveDx[i] *= scaleFactor;
+ _resolveDy[i] *= scaleFactor;
+ }
+ }
+ }
+ }
+
+ private static double GetAdvance(CharacterLayout characterLayout)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void ResolveTextSpanTextLength(TextSpan textSpan, string text, ref bool in_text_path)
+ {
+ double a = Double.PositiveInfinity;
+ double b = Double.NegativeInfinity;
+ int i = textSpan.GetFirstCharacter().GlobalIndex;
+ int j = textSpan.GetLastCharacter().GlobalIndex;
+ for (int k = i; k <= j; k++)
+ {
+ if (!_result[k].Addressable)
+ {
+ continue;
+ }
+ if (_result[k].Character == '\r' || _result[k].Character == '\n')
+ {
+ //No adjustments due to ‘textLength’ are made to a node with a forced line break.
+ return;
+ }
+ double pos = _horizontal ? _result[k].X : _result[k].Y;
+ double advance = GetAdvance(_result[k]); //This advance will be negative for RTL horizontal text.
+
+ a = Math.Min(Math.Min(a, pos), pos + advance);
+ b = Math.Max(Math.Max(b, pos), pos + advance);
+
+ }
+ if (!Double.IsPositiveInfinity(a))
+ {
+ double delta = textSpan.TextLength.Value.Value - (b - a);
+ int n = 0;// textSpan.GetTypographicCharacterCount();
+ int resolvedDescendantNodes = GetResolvedDescendantNodeCount(textSpan, ref n);
+ n += resolvedDescendantNodes - 1;//Each resolved descendant node is treated as if it were a single typographic character in this context.
+ var δ = delta / n;
+ double shift = 0;
+ for (int k = i; k <= j; k++)
+ {
+ if (_horizontal)
+ {
+ _result[k].X += shift;
+ }
+ else
+ {
+ _result[k].Y += shift;
+ }
+ if (!_result[k].Middle && IsNotACharacterInAResolvedDescendantNodeOtherThanTheFirstCharacter(textSpan, _result[k]))
+ {
+ shift += δ;
+ }
+ }
+ }
+ }
+
+ internal int GetResolvedDescendantNodeCount(ITextNode node, ref int n)
+ {
+ int resolvedDescendantNodes = 0;
+ if (node is TextSpan textSpan)
+ {
+ n = textSpan.Children.Count;
+ for (int c = 0; c < textSpan.Children.Count; c++)
+ {
+ if (textSpan.Children[c].GetText() is string ccontent)
+ {
+ n += String.IsNullOrEmpty(ccontent) ? 0 : ccontent.Length;
+ }
+ else
+ {
+ _result[n].FirstCharacterInResolvedDescendant = true;
+ resolvedDescendantNodes++;
+ }
+ }
+ }
+ else if (node is TextString textString)
+ {
+ n = textString.GetLength();
+ }
+ return resolvedDescendantNodes;
+ }
+ private static bool IsNotACharacterInAResolvedDescendantNodeOtherThanTheFirstCharacter(ITextNode textNode, CharacterLayout character)
+ {
+ throw new NotImplementedException("This method needs to be implemented based on the specific rules for resolved descendant nodes.");
+ }
+
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/TextPath.cs b/Source/SVGImage/SVG/Shapes/TextPath.cs
new file mode 100644
index 0000000..e31749f
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/TextPath.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Xml;
+
+namespace SVGImage.SVG.Shapes
+{
+ ///
+ /// A placeholder class for TextPath.
+ ///
+ internal class TextPath : TextShapeBase, ITextChild
+ {
+ protected TextPath(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent)
+ {
+ throw new NotImplementedException("TextPath is not yet implemented.");
+ }
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/TextRender.cs b/Source/SVGImage/SVG/Shapes/TextRender.cs
new file mode 100644
index 0000000..cdd1a12
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/TextRender.cs
@@ -0,0 +1,187 @@
+using System.Collections.Generic;
+using System.Windows.Media;
+using System.Windows;
+
+namespace SVGImage.SVG.Shapes
+{
+ using Utils;
+ using System.Linq;
+ using System.Windows.Markup;
+
+ ///
+ /// This class is responsible for rendering text shapes in SVG.
+ ///
+ public sealed partial class TextRender : TextRenderBase
+ {
+ public override GeometryGroup BuildTextGeometry(TextShape text)
+ {
+
+ using(TextRenderState state = new TextRenderState())
+ {
+ if (!state.Setup(text))
+ {
+ return null; // No characters to render
+ }
+ return CreateGeometry(text, state);
+ }
+ }
+ private static GeometryGroup CreateGeometry(TextShape root, TextRenderState state)
+ {
+ state.Resolve(root);
+
+ List textStrings = new List();
+ TextStyleStack textStyleStacks = new TextStyleStack();
+ PopulateTextStrings(textStrings, root, textStyleStacks);
+ GeometryGroup mainGeometryGroup = new GeometryGroup();
+ var baselineOrigin = new Point(root.X.FirstOrDefault().Value, root.Y.FirstOrDefault().Value);
+ var isSideways = root.WritingMode == WritingMode.HorizontalTopToBottom;
+ 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)
+ {
+ var runGeometry = run.BuildGeometry();
+ geometryGroup.Children.Add(runGeometry);
+ if (textStyle.TextDecoration != null && textStyle.TextDecoration.Count > 0)
+ {
+ GetTextDecorations(geometryGroup, textStyle, font, baselineOrigin, out List backgroundDecorations, out List foregroundDecorations);
+ foreach (var decoration in backgroundDecorations)
+ {
+ //Underline and OverLine should be drawn behind the text
+ geometryGroup.Children.Insert(0, new RectangleGeometry(decoration));
+ }
+ foreach (var decoration in foregroundDecorations)
+ {
+ //Strikethrough should be drawn on top of the text
+ geometryGroup.Children.Add(new RectangleGeometry(decoration));
+ }
+ }
+ mainGeometryGroup.Children.Add(geometryGroup);
+ }
+ baselineOrigin = newBaseline;
+ }
+
+ mainGeometryGroup.Transform = root.Transform;
+ return mainGeometryGroup;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ /// 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)
+ {
+ backgroundDecorations = new List();
+ foregroundDecorations = new List();
+ double decorationPos = 0;
+ double decorationThickness = 0;
+ double fontSize = textStyle.FontSize;
+ double baselineY = baselineOrigin.Y;
+ foreach(TextDecorationLocation textDecorationLocation in textStyle.TextDecoration.Select(td=>td.Location))
+ {
+ if (textDecorationLocation == TextDecorationLocation.Strikethrough)
+ {
+ decorationPos = baselineY - (font.StrikethroughPosition * fontSize);
+ decorationThickness = font.StrikethroughThickness * fontSize;
+ Rect bounds = new Rect(geometryGroup.Bounds.Left, decorationPos, geometryGroup.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);
+ 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);
+ backgroundDecorations.Add(bounds);
+ }
+ }
+ }
+
+ private static GlyphRun CreateRun(TextString textString, TextRenderState state, Typeface font, bool isSideways, Point baselineOrigin, out Point newBaseline)
+ {
+ var textStyle = textString.TextStyle;
+ var characterInfos = textString.GetCharacters();
+ if(characterInfos is null ||characterInfos.Length == 0)
+ {
+ newBaseline = baselineOrigin;
+ return null;
+ }
+ if (!font.TryGetGlyphTypeface(out var glyphFace))
+ {
+ newBaseline = baselineOrigin;
+ return null;
+ }
+ string deviceFontName = null;
+ IList clusterMap = null;
+ IList caretStops = null;
+ XmlLanguage language = null;
+ var glyphOffsets = characterInfos.Select(c => new Point(c.DX, -c.DY)).ToList();
+ 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();
+
+
+ if (characterInfos[0].DoesPositionX)
+ {
+ baselineOrigin.X = characterInfos[0].X;
+ }
+ if (characterInfos[0].DoesPositionY)
+ {
+ baselineOrigin.Y = characterInfos[0].Y;
+ }
+
+ 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();
+ var newY = baselineOrigin.Y ;
+
+ newBaseline = new Point(newX, newY);
+ return run;
+ }
+
+
+
+ private static void PopulateTextStrings(List textStrings, ITextNode node, TextStyleStack textStyleStacks)
+ {
+ if(node is TextShapeBase span)
+ {
+ textStyleStacks.Push(span.TextStyle);
+ foreach (var child in span.Children)
+ {
+ PopulateTextStrings(textStrings, child, textStyleStacks);
+ }
+ _ = textStyleStacks.Pop();
+ }
+ else if(node is TextString textString)
+ {
+ textString.TextStyle = textStyleStacks.Peek();
+ textStrings.Add(textString);
+ }
+ }
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/TextRenderBase.cs b/Source/SVGImage/SVG/Shapes/TextRenderBase.cs
new file mode 100644
index 0000000..ccec0cf
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/TextRenderBase.cs
@@ -0,0 +1,24 @@
+using System.Windows.Media;
+using System.Windows;
+
+namespace SVGImage.SVG.Shapes
+{
+ 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)
+ {
+ obj.SetValue(TSpanElementProperty, value);
+ }
+ public static ITextNode GetElement(DependencyObject obj)
+ {
+ return (ITextNode)obj.GetValue(TSpanElementProperty);
+ }
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/TextRenderState.cs b/Source/SVGImage/SVG/Shapes/TextRenderState.cs
new file mode 100644
index 0000000..a4484d3
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/TextRenderState.cs
@@ -0,0 +1,362 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace SVGImage.SVG.Shapes
+{
+ public sealed partial class TextRender
+ {
+ ///
+ /// Represents the state of the text rendering process, including character layout and position resolution.
+ /// This class is responsible for setting up the text layout, resolving positions and transformations, and applying them to the characters.
+ ///
+ private sealed class TextRenderState : IDisposable
+ {
+ private bool _disposedValue;
+ private CharacterLayout[] _characters;
+ private double[] _resolvedX;
+ private double[] _resolvedY;
+ private double[] _resolvedDx;
+ private double[] _resolvedDy;
+ private double[] _resolvedRotate;
+ private int[] _xBaseIndicies;
+ private int[] _yBaseIndicies;
+ public int BidiLevel { get; private set; } = 0;
+ private void ResetState()
+ {
+ _characters = null;
+ _resolvedX = null;
+ _resolvedY = null;
+ _resolvedDx = null;
+ _resolvedDy = null;
+ _resolvedRotate = null;
+ _xBaseIndicies = null;
+ _yBaseIndicies = null;
+ }
+ ///
+ /// Initializes the text render state with the root TextShape.
+ ///
+ ///
+ /// The root TextShape to process. This should contain all the text nodes and their children.
+ ///
+ ///
+ /// Returns true if the setup was successful, false if there were no characters to process.
+ ///
+ public bool Setup(TextShape root)
+ {
+ string text = root.GetText();
+ int globalIndex = 0;
+ SetGlobalIndicies(root, ref globalIndex);
+ _characters = root.GetCharacters();
+ SetFlagsAndAssignInitialPositions(root, text);
+ if (_characters.Length == 0)
+ {
+ return false;
+ }
+ return true;
+ }
+ ///
+ /// Creates an array of a specified length, filled with a specified element.
+ ///
+ ///
+ /// The type of the element to fill the array with. Must be a struct type.
+ ///
+ ///
+ /// The length of the array to create.
+ ///
+ ///
+ /// The element to fill the array with. This should be a value type (struct).
+ ///
+ ///
+ /// Returns an array of type T, filled with the specified element.
+ ///
+ private static T[] CreateRepeatedArray(int count, T element) where T : struct
+ {
+ var result = new T[count];
+ for (int i = 0; i < count; i++)
+ {
+ result[i] = element;
+ }
+ return result;
+ }
+ ///
+ /// Initializes the arrays used for resolving text positions and transformations.
+ ///
+ ///
+ /// The length of the arrays to initialize. This should match the number of characters in the text.
+ ///
+ private void InitializeResolveArrays(int length)
+ {
+ _xBaseIndicies = CreateRepeatedArray(length, -1);
+ _yBaseIndicies = CreateRepeatedArray(length, -1);
+ if (length > 0)
+ {
+ _xBaseIndicies[0] = 0;
+ _yBaseIndicies[0] = 0;
+ }
+ _resolvedX = CreateRepeatedArray(length, double.NaN);
+ _resolvedY = CreateRepeatedArray(length, double.NaN);
+ _resolvedDx = CreateRepeatedArray(length, 0d);
+ _resolvedDy = CreateRepeatedArray(length, 0d);
+ _resolvedRotate = CreateRepeatedArray(length, double.NaN);
+ }
+
+ ///
+ /// Recursively resolves the text span's position and offset values.
+ ///
+ ///
+ /// The TextShapeBase to resolve. This should contain the text span's position and offset values.
+ ///
+ public void Resolve(TextShapeBase textSpan)
+ {
+ string text = textSpan.GetText();
+ InitializeResolveArrays(text.Length);
+ ResolveInternal(textSpan);
+ ApplyResolutions();
+ }
+
+ ///
+ /// Recursively resolves the text span's position and offset values.
+ ///
+ ///
+ /// The TextShapeBase to resolve. This should contain the text span's position and offset values.
+ ///
+ private void ResolveInternal(TextShapeBase textSpan)
+ {
+ int index = textSpan.GetFirstCharacter().GlobalIndex;
+ LengthPercentageOrNumberList x = textSpan.X;
+ LengthPercentageOrNumberList y = textSpan.Y;
+ LengthPercentageOrNumberList dx = textSpan.DX;
+ LengthPercentageOrNumberList dy = textSpan.DY;
+ List rotate = textSpan.Rotate;
+
+ var arrays = new List>();
+ arrays.Add(new Tuple(x, _resolvedX));
+ arrays.Add(new Tuple(y, _resolvedY));
+ arrays.Add(new Tuple(dx, _resolvedDx));
+ arrays.Add(new Tuple(dy, _resolvedDy));
+
+ foreach (var tuple in arrays)
+ {
+ var list = tuple.Item1;
+ var resolvedArray = tuple.Item2;
+ for (int i = 0; i < list.Count; i++)
+ {
+ // Check if the index is within bounds of the resolved array
+ if (index + i >= resolvedArray.Length)
+ {
+ break;
+ }
+ resolvedArray[index + i] = list[i].Value;
+ }
+ }
+
+ for (int i = 0; i < rotate.Count; i++)
+ {
+ // Check if the index is within bounds of the resolved array
+ if (index + i >= _resolvedRotate.Length)
+ {
+ break;
+ }
+ _resolvedRotate[index + i] = rotate[i];
+ }
+ foreach (var child in textSpan.Children.OfType())
+ {
+ ResolveInternal(child);
+ }
+ }
+ ///
+ /// Recursively sets the global indices for each character in the text node.
+ ///
+ ///
+ /// The text node to process. This should be a or containing characters.
+ ///
+ ///
+ /// A reference to the global index counter. This will be incremented for each character processed.
+ ///
+ private static void SetGlobalIndicies(ITextNode textNode, ref int globalIndex)
+ {
+ if (textNode is TextShapeBase textNodeBase)
+ {
+ foreach (var child in textNodeBase.Children)
+ {
+ SetGlobalIndicies(child, ref globalIndex);
+ }
+ }
+ else if (textNode is TextString textString)
+ {
+ foreach (var c in textString.Characters)
+ {
+ c.GlobalIndex = globalIndex++;
+ }
+ }
+ }
+ ///
+ /// Fills in gaps in the resolved position and rotation arrays.
+ ///
+ private void FillInGaps()
+ {
+ FillInGaps(_resolvedX, 0d, _xBaseIndicies);
+ FillInGaps(_resolvedY, 0d, _yBaseIndicies);
+ FillInGaps(_resolvedRotate, 0d);
+ }
+ ///
+ /// Fills in gaps in the specified list with the initial value or the last known value.
+ ///
+ ///
+ /// The list to fill in gaps for. This should be an array of doubles.
+ ///
+ ///
+ /// The initial value to use for filling gaps. If null, the first value in the list will be used, even if it is NaN.
+ ///
+ ///
+ /// An optional array of base indices to track the last known index for each position in the list.
+ ///
+ private static void FillInGaps(double[] list, double? initialValue = null, int[] baseIndicies = null)
+ {
+ if (list == null || list.Length == 0)
+ {
+ return;
+ }
+ if (Double.IsNaN(list[0]) && initialValue.HasValue && !Double.IsNaN(initialValue.Value))
+ {
+ list[0] = initialValue.Value;
+ }
+ double current = list[0];
+ int currentBaseIndex = 0;
+ for (int i = 1; i < list.Length; i++)
+ {
+ if (Double.IsNaN(list[i]))
+ {
+ list[i] = current;
+ }
+ else
+ {
+ current = list[i];
+ currentBaseIndex = i;
+ }
+ if (baseIndicies != null)
+ {
+ baseIndicies[i] = currentBaseIndex;
+ }
+ }
+ }
+ ///
+ /// Applies the resolved positions and transformations to the characters.
+ ///
+ private void ApplyResolutions()
+ {
+ FillInGaps();
+ for (int i = 0; i < _characters.Length; i++)
+ {
+ int xBaseIndex = _xBaseIndicies[i];
+ int yBaseIndex = _yBaseIndicies[i];
+ _characters[i].X = _resolvedX[xBaseIndex];
+ _characters[i].Y = _resolvedY[yBaseIndex];
+ _characters[i].DX = _resolvedDx.Skip(xBaseIndex).Take(i - xBaseIndex + 1).Sum();
+ _characters[i].DY = _resolvedDy.Skip(yBaseIndex).Take(i - yBaseIndex + 1).Sum();
+ _characters[i].Rotation = _resolvedRotate[i];
+ _characters[i].DoesPositionX = _xBaseIndicies[_characters[i].GlobalIndex] == _characters[i].GlobalIndex;
+ _characters[i].DoesPositionY = _yBaseIndicies[_characters[i].GlobalIndex] == _characters[i].GlobalIndex;
+ }
+
+ }
+ ///
+ /// Preliminary, Need Implementation
+ ///
+ ///
+ ///
+ private static bool IsNonRenderedCharacter(char c)
+ {
+ // Check for non-rendered characters like zero-width space, etc.
+ return char.IsControl(c) || char.IsWhiteSpace(c) && c != ' ';
+ }
+
+ private static bool IsBidiControlCharacter(char c)
+ {
+ // Check for Bidi control characters
+ return c == '\u2066' || c == '\u2067' || c == '\u2068' || c == '\u2069' ||
+ c == '\u200E' || c == '\u200F' || c == '\u202A' || c == '\u202B' || c == '\u202C' || c == '\u202D' || c == '\u202E';
+ }
+
+ ///
+ /// discarded during layout due to being a collapsed white space character, a soft hyphen character, collapsed segment break, or a bidi control character
+ ///
+ ///
+ ///
+ private static bool WasDiscardedDuringLayout(char c)
+ {
+ return IsBidiControlCharacter(c) ||
+ c == '\u00AD' || // Soft hyphen
+ c == '\u200B' || // Zero-width space
+ c == '\u200C' || // Zero-width non-joiner
+ c == '\u200D' || // Zero-width joiner
+ char.IsWhiteSpace(c) && c != ' '; // Collapsed whitespace characters
+ }
+
+ private static bool IsAddressable(int index, char c, int beginningCharactersTrimmed, int startOfTrimmedEnd)
+ {
+ return !IsNonRenderedCharacter(c) &&
+ !WasDiscardedDuringLayout(c) &&
+ index >= beginningCharactersTrimmed &&
+ index < startOfTrimmedEnd;
+ }
+ private static readonly Regex _trimmedWhitespace = new Regex(@"(?^\s*).*(?\s*$)", RegexOptions.Compiled | RegexOptions.Singleline);
+ private static readonly Regex _lineStarts = new Regex(@"^ *\S", RegexOptions.Compiled | RegexOptions.Multiline);
+ private static bool IsTypographicCharacter(char c, int index, string text)
+ {
+ return !IsNonRenderedCharacter(c); //It's not clear what a typographic character is in this context, so we assume all non-rendered characters are not typographic.
+ }
+ private static HashSet GetLineBeginnings(string text)
+ {
+ HashSet lineStartIndicies = new HashSet();
+ var lineStarts = _lineStarts.Matches(text);
+ foreach (Match lineStart in lineStarts)
+ {
+ lineStartIndicies.Add(lineStart.Index + lineStart.Length - 1);
+ }
+ return lineStartIndicies;
+ }
+ public void SetFlagsAndAssignInitialPositions(TextShape root, string text)
+ {
+ var trimmedText = _trimmedWhitespace.Match(text);
+ int beginningCharactersTrimmed = trimmedText.Groups["Start"].Success ? trimmedText.Groups["Start"].Length : 0;
+ int endingCharactersTrimmed = trimmedText.Groups["End"].Success ? trimmedText.Groups["End"].Length : 0;
+ int startOfTrimmedEnd = text.Length - endingCharactersTrimmed;
+ var lineBeginnings = GetLineBeginnings(text);
+ for (int i = 0; i < _characters.Length; i++)
+ {
+ var c = _characters[i];
+ bool isTypographic = IsTypographicCharacter(text[i], i, text);
+ c.Addressable = IsAddressable(i, text[i], beginningCharactersTrimmed, startOfTrimmedEnd);
+ c.Middle = i > 0 && isTypographic;
+ c.AnchoredChunk = lineBeginnings.Contains(i);
+ }
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ ResetState();
+ }
+
+ _disposedValue = true;
+ }
+ }
+
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ }
+}
diff --git a/Source/SVGImage/SVG/Shapes/TextShape.cs b/Source/SVGImage/SVG/Shapes/TextShape.cs
new file mode 100644
index 0000000..da28c7f
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/TextShape.cs
@@ -0,0 +1,17 @@
+using System.Xml;
+
+namespace SVGImage.SVG.Shapes
+{
+ public class TextShape : TextShapeBase
+ {
+ public TextShape(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent)
+ {
+
+ }
+
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/TextShapeBase.cs b/Source/SVGImage/SVG/Shapes/TextShapeBase.cs
new file mode 100644
index 0000000..00e2053
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/TextShapeBase.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Collections.Generic;
+using System.Xml;
+
+namespace SVGImage.SVG.Shapes
+{
+ using System.Linq;
+ using System.Diagnostics;
+ using System.Text;
+
+ [DebuggerDisplay("{DebugDisplayText}")]
+ public class TextShapeBase: Shape, ITextNode
+ {
+ protected TextShapeBase(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent)
+ {
+ }
+
+ private string DebugDisplayText => GetDebugDisplayText(new StringBuilder());
+ private string GetDebugDisplayText(StringBuilder sb)
+ {
+ if (Children.Count == 0)
+ {
+ return "";
+ }
+ foreach(var child in Children)
+ {
+ if (child is TextString textString)
+ {
+ sb.Append(textString.Text);
+ }
+ else if (child is TextSpan textSpan)
+ {
+ sb.Append('(');
+ textSpan.GetDebugDisplayText(sb);
+ sb.Append(')');
+ }
+ }
+
+ return sb.ToString();
+ }
+
+
+
+ public LengthPercentageOrNumberList X { get; protected set; }
+ public LengthPercentageOrNumberList Y { get; protected set; }
+ public LengthPercentageOrNumberList DX { get; protected set; }
+ public LengthPercentageOrNumberList DY { get; protected set; }
+ public WritingMode WritingMode { get; set; }
+ public List Attributes { get; set; } = new List();
+ public List Rotate { get; protected set; } = new List();
+ public LengthPercentageOrNumber? TextLength { get; set; }
+ public LengthAdjustment LengthAdjust { get; set; } = LengthAdjustment.Spacing;
+ public List Children { get; } = new List();
+ public CharacterLayout FirstCharacter => GetFirstCharacter();
+ public CharacterLayout LastCharacter => GetLastCharacter();
+ public string Text => GetText();
+ public int Length => GetLength();
+
+
+
+ public CharacterLayout[] GetCharacters()
+ {
+ return Children.SelectMany(c => c.GetCharacters()).ToArray();
+ }
+
+ public CharacterLayout GetFirstCharacter()
+ {
+ foreach(var child in Children)
+ {
+ if (child.GetFirstCharacter() is CharacterLayout firstChar)
+ {
+ return firstChar;
+ }
+ }
+ throw new InvalidOperationException("No characters found in text node.");
+ }
+ public CharacterLayout GetLastCharacter()
+ {
+ for (int i = Children.Count - 1; i >= 0; i--)
+ {
+ if (Children[i].GetLastCharacter() is CharacterLayout LastChar)
+ {
+ return LastChar;
+ }
+ }
+ throw new InvalidOperationException("No characters found in text node.");
+ }
+
+ public int GetLength()
+ {
+ return Children.Sum(c => c.GetLength());
+ }
+
+ public string GetText()
+ {
+ return string.Concat(Children.Select(c => c.GetText()));
+ }
+
+ protected override void ParseAtStart(SVG svg, XmlNode node)
+ {
+ base.ParseAtStart(svg, node);
+
+ foreach (XmlAttribute attr in node.Attributes)
+ {
+ switch (attr.Name)
+ {
+ case "x":
+ X = new LengthPercentageOrNumberList(this, attr.Value, LengthOrientation.Horizontal);
+ break;
+ case "y":
+ Y = new LengthPercentageOrNumberList(this, attr.Value, LengthOrientation.Vertical);
+ break;
+ case "dx":
+ DX = new LengthPercentageOrNumberList(this, attr.Value, LengthOrientation.Horizontal);
+ break;
+ case "dy":
+ DY = new LengthPercentageOrNumberList(this, attr.Value, LengthOrientation.Vertical);
+ break;
+ case "rotate":
+ Rotate = attr.Value.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(v => double.Parse(v)).ToList();
+ break;
+ case "textLength":
+ TextLength = LengthPercentageOrNumber.Parse(this, attr.Value);
+ break;
+ case "lengthAdjust":
+ LengthAdjust = Enum.TryParse(attr.Value, true, out LengthAdjustment adj) ? adj : LengthAdjustment.Spacing;
+ break;
+ case "writing-mode":
+ switch (attr.Value)
+ {
+ case "horizontal-tb":
+ case "lr":
+ case "lr-tb":
+ case "rl":
+ case "rl-tb":
+ WritingMode = WritingMode.HorizontalTopToBottom;
+ break;
+ case "vertical-rl":
+ case "tb":
+ case "tb-rl":
+ WritingMode = WritingMode.VerticalRightToLeft;
+ break;
+ case "vertical-lr":
+ WritingMode = WritingMode.VerticalLeftToRight;
+ break;
+ default:
+ Debug.WriteLine($"Unknown writing-mode: {attr.Value}");
+ break;
+ }
+ break;
+ default:
+ Attributes.Add(new StyleItem(attr.Name, attr.Value));
+ break;
+ }
+ }
+ if(X is null)
+ {
+ X = LengthPercentageOrNumberList.Empty(this, LengthOrientation.Horizontal);
+ }
+ if (Y is null)
+ {
+ Y = LengthPercentageOrNumberList.Empty(this, LengthOrientation.Vertical);
+ }
+ if (DX is null)
+ {
+ DX = LengthPercentageOrNumberList.Empty(this, LengthOrientation.Horizontal);
+ }
+ if (DY is null)
+ {
+ DY = LengthPercentageOrNumberList.Empty(this, LengthOrientation.Vertical);
+ }
+
+ ParseChildren(svg, node);
+ }
+
+ protected void ParseChildren(SVG svg, XmlNode node)
+ {
+ foreach (XmlNode child in node.ChildNodes)
+ {
+ if (child.NodeType == XmlNodeType.Text || child.NodeType == XmlNodeType.CDATA)
+ {
+ var text = child.InnerText.Trim();
+ if (!string.IsNullOrWhiteSpace(text))
+ {
+ Children.Add(new TextString(this, text));
+ }
+ }
+ else if (child.NodeType == XmlNodeType.Element && child.Name == "tspan")
+ {
+ var span = new TextSpan(svg, child, this);
+ Children.Add(span);
+ }
+ // Future support for , , etc. could go here
+ }
+ }
+
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/TextSpan.cs b/Source/SVGImage/SVG/Shapes/TextSpan.cs
new file mode 100644
index 0000000..c6cd990
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/TextSpan.cs
@@ -0,0 +1,17 @@
+using System.Xml;
+
+namespace SVGImage.SVG.Shapes
+{
+ public class TextSpan : TextShapeBase, ITextChild
+ {
+
+ public TextSpan(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent)
+ {
+ }
+
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/TextString.cs b/Source/SVGImage/SVG/Shapes/TextString.cs
new file mode 100644
index 0000000..7b0af19
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/TextString.cs
@@ -0,0 +1,68 @@
+namespace SVGImage.SVG.Shapes
+{
+ using System.Linq;
+ using System.Text.RegularExpressions;
+ using System.Diagnostics;
+
+ [DebuggerDisplay("{Text}")]
+ ///
+ /// The leaf of the Text tree
+ ///
+ public class TextString : ITextChild
+ {
+ private static readonly Regex _trimmedWhitespace = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.Singleline);
+ ///
+ /// Represents the layout info for the string of text owned by this TextString.
+ ///
+ public CharacterLayout[] Characters { get; set; }
+ public Shape Parent { get; set; }
+ ///
+ /// The index of this TextString in the root TextShape. This is set by the .
+ ///
+ public int Index { get; internal set; }
+ public TextString(Shape parent, string text)
+ {
+ Parent = parent;
+ string trimmed = _trimmedWhitespace.Replace(text.Trim(), " ");
+ Characters = new CharacterLayout[trimmed.Length];
+ for(int i = 0; i < trimmed.Length; i++)
+ {
+ var c = trimmed[i];
+ Characters[i] = new CharacterLayout(c);
+ }
+ }
+ public CharacterLayout GetFirstCharacter()
+ {
+ return Characters.FirstOrDefault();
+ }
+ public CharacterLayout GetLastCharacter()
+ {
+ return Characters.LastOrDefault();
+ }
+ public CharacterLayout FirstCharacter => GetFirstCharacter();
+ public CharacterLayout LastCharacter => GetLastCharacter();
+ public string Text => GetText();
+ public int Length => GetLength();
+
+ public TextStyle TextStyle { get; internal set; }
+
+ public string GetText()
+ {
+ return new string(Characters.Select(c => c.Character).ToArray());
+ }
+
+ public int GetLength()
+ {
+ return Characters.Length;
+ }
+
+ public CharacterLayout[] GetCharacters()
+ {
+ return Characters;
+ }
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Shapes/WritingMode.cs b/Source/SVGImage/SVG/Shapes/WritingMode.cs
new file mode 100644
index 0000000..9f2f342
--- /dev/null
+++ b/Source/SVGImage/SVG/Shapes/WritingMode.cs
@@ -0,0 +1,41 @@
+namespace SVGImage.SVG.Shapes
+{
+ ///
+ /// The ‘writing-mode’ property specifies whether the initial inline-progression-direction for a ‘text’ element shall be left-to-right, right-to-left, or top-to-bottom.
+ /// The ‘writing-mode’ property applies only to ‘text’ elements; the property is ignored for ‘tspan’, ‘tref’, ‘altGlyph’ and ‘textPath’ sub-elements.
+ /// (Note that the inline-progression-direction can change within a ‘text’ element due to the Unicode bidirectional algorithm and properties ‘direction’ and ‘unicode-bidi’.
+ /// For more on bidirectional text, see Relationship with bidirectionality.)
+ ///
+ public enum WritingMode
+ {
+ ///
+ /// Inherits the writing mode from the parent element.
+ ///
+ None = 0,
+ ///
+ /// This value defines a top-to-bottom block flow direction. Both the writing mode and the typographic mode are horizontal.
+ ///
+ ///
+ /// Set for atrributes with values 'horizontal-tb', 'lr', 'lr-tb', 'rl', and 'rl-tb'.
+ ///
+ HorizontalTopToBottom,
+ ///
+ /// This value defines a right-to-left block flow direction. Both the writing mode and the typographic mode are vertical.
+ ///
+ ///
+ /// Set for atrributes with values 'vertical-rl', 'tb-rl', 'tb'.
+ ///
+ VerticalRightToLeft,
+ ///
+ /// This value defines a left-to-right block flow direction. Both the writing mode and the typographic mode are vertical.
+ ///
+ ///
+ /// Set for atrributes with value 'vertical-lr'.
+ ///
+ VerticalLeftToRight,
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/TextRender.cs b/Source/SVGImage/SVG/TextRender2.cs
similarity index 63%
rename from Source/SVGImage/SVG/TextRender.cs
rename to Source/SVGImage/SVG/TextRender2.cs
index 8ce868f..b9289d9 100644
--- a/Source/SVGImage/SVG/TextRender.cs
+++ b/Source/SVGImage/SVG/TextRender2.cs
@@ -2,15 +2,15 @@
using System.Windows.Media;
using System.Windows;
using System.Reflection;
+using SVGImage.SVG.Shapes;
+using System.Linq;
+using SVGImage.SVG.Utils;
namespace SVGImage.SVG
{
- static class TextRender
+ public class TextRender2 : TextRenderBase
{
- private static int dpiX = 0;
- private static int dpiY = 0;
-
- static public GeometryGroup BuildTextGeometry(TextShape shape)
+ public override GeometryGroup BuildTextGeometry(TextShape shape)
{
return BuildGlyphRun(shape, 0, 0);
}
@@ -20,60 +20,45 @@ static public GeometryGroup BuildTextGeometry(TextShape shape)
static GeometryGroup BuildGlyphRun(TextShape shape, double xoffset, double yoffset)
{
GeometryGroup gp = new GeometryGroup();
- double totalwidth = 0;
- if (shape.TextSpan == null)
- {
- string txt = shape.Text;
- gp.Children.Add(BuildGlyphRun(shape.TextStyle, txt, shape.X, shape.Y, ref totalwidth));
- return gp;
- }
return BuildTextSpan(shape);
}
static GeometryGroup BuildTextSpan(TextShape shape)
{
- double x = shape.X;
- double y = shape.Y;
+ double x = shape.X.FirstOrDefault().Value;
+ double y = shape.Y.FirstOrDefault().Value;
GeometryGroup gp = new GeometryGroup();
- BuildTextSpan(gp, shape.TextStyle, shape.TextSpan, ref x, ref y);
+ BuildTextSpan(gp, shape.TextStyle, shape, ref x, ref y);
return gp;
}
- public static DependencyProperty TSpanElementProperty = DependencyProperty.RegisterAttached(
- "TSpanElement", typeof(TextSpan), typeof(DependencyObject));
- public static void SetElement(DependencyObject obj, TextSpan value)
- {
- obj.SetValue(TSpanElementProperty, value);
- }
- public static TextSpan GetElement(DependencyObject obj)
- {
- return (TextSpan)obj.GetValue(TSpanElementProperty);
- }
+
- static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle,
- TextSpan tspan, ref double x, ref double y)
+ static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle,
+ TextShapeBase tspan, ref double x, ref double y)
{
- foreach (TextSpan child in tspan.Children)
+ foreach (ITextNode child in tspan.Children)
{
- if (child.ElementType == TextSpan.eElementType.Text)
+ if (child is TextString textString)
{
- string txt = child.Text;
+ string txt = textString.Text;
double totalwidth = 0;
double baseline = y;
- if (child.TextStyle.BaseLineShift == "sub")
- baseline += child.TextStyle.FontSize * 0.5; /* * cap height ? fontSize*/;
- if (child.TextStyle.BaseLineShift == "super")
- baseline -= tspan.TextStyle.FontSize + (child.TextStyle.FontSize * 0.25)/*font.CapsHeight * fontSize*/;
+ if (textString.TextStyle.BaseLineShift == "sub")
+ baseline += textString.TextStyle.FontSize * 0.5; /* * cap height ? fontSize*/;
+ if (textString.TextStyle.BaseLineShift == "super")
+ baseline -= tspan.TextStyle.FontSize + (textString.TextStyle.FontSize * 0.25)/*font.CapsHeight * fontSize*/;
- Geometry gm = BuildGlyphRun(child.TextStyle, txt, x, baseline, ref totalwidth);
- TextRender.SetElement(gm, child);
+ Geometry gm = BuildGlyphRun(textString.TextStyle, txt, x, baseline, ref totalwidth);
+ TextRender2.SetElement(gm, textString);
gp.Children.Add(gm);
x += totalwidth;
- continue;
}
- if (child.ElementType == TextSpan.eElementType.Tag)
- BuildTextSpan(gp, textStyle, child, ref x, ref y);
+ else if (child is TextShapeBase childTspan)
+ {
+ BuildTextSpan(gp, textStyle, childTspan, ref x, ref y);
+ }
}
}
@@ -82,19 +67,10 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double
if (string.IsNullOrEmpty(text))
return new GeometryGroup();
- if (dpiX == 0 || dpiY == 0)
- {
- var sysPara = typeof(SystemParameters);
- var dpiXProperty = sysPara.GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static);
- var dpiYProperty = sysPara.GetProperty("Dpi", BindingFlags.NonPublic | BindingFlags.Static);
-
- dpiX = (int)dpiXProperty.GetValue(null, null);
- dpiY = (int)dpiYProperty.GetValue(null, null);
- }
double fontSize = textStyle.FontSize;
GlyphRun glyphs = null;
- Typeface font = new Typeface(new FontFamily(textStyle.FontFamily),
- textStyle.Fontstyle,
+ Typeface font = new Typeface(new FontFamily(textStyle.FontFamily),
+ textStyle.Fontstyle,
textStyle.Fontweight,
FontStretch.FromOpenTypeStretch(9),
new FontFamily("Arial Unicode MS"));
@@ -102,12 +78,10 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double
double baseline = y;
if (font.TryGetGlyphTypeface(out glyphFace))
{
-#if DOTNET40 || DOTNET45 || DOTNET46
- glyphs = new GlyphRun();
+#if DPI_AWARE
+ glyphs = new GlyphRun((float)DpiUtil.PixelsPerDip);
#else
- var dpiScale = new DpiScale(dpiX, dpiY);
-
- glyphs = new GlyphRun((float)dpiScale.PixelsPerDip);
+ glyphs = new GlyphRun();
#endif
((System.ComponentModel.ISupportInitialize)glyphs).BeginInit();
glyphs.GlyphTypeface = glyphFace;
@@ -116,7 +90,7 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double
List glyphIndices = new List();
List advanceWidths = new List();
totalwidth = 0;
- char[] charsToSkip = new char[] {'\t', '\r', '\n'};
+ char[] charsToSkip = new char[] { '\t', '\r', '\n' };
for (int i = 0; i < text.Length; ++i)
{
char textchar = text[i];
@@ -124,15 +98,15 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double
//if (charsToSkip.Any(item => item == codepoint))
// continue;
ushort glyphIndex;
- if (glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out glyphIndex) == false)
+ if (!glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out glyphIndex))
continue;
textChars.Add(textchar);
double glyphWidth = glyphFace.AdvanceWidths[glyphIndex];
glyphIndices.Add(glyphIndex);
advanceWidths.Add(glyphWidth * fontSize + textStyle.LetterSpacing);
if (char.IsWhiteSpace(textchar))
- advanceWidths[advanceWidths.Count-1] += textStyle.WordSpacing;
- totalwidth += advanceWidths[advanceWidths.Count-1];
+ advanceWidths[advanceWidths.Count - 1] += textStyle.WordSpacing;
+ totalwidth += advanceWidths[advanceWidths.Count - 1];
}
glyphs.Characters = textChars.ToArray();
glyphs.GlyphIndices = glyphIndices.ToArray();
@@ -171,7 +145,7 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double
}
if (textStyle.TextDecoration[0].Location == TextDecorationLocation.OverLine)
{
- decorationPos = baseline - fontSize;
+ decorationPos = baseline - fontSize;
decorationThickness = font.StrikethroughThickness * fontSize;
}
Rect bounds = new Rect(gp.Bounds.Left, decorationPos, gp.Bounds.Width, decorationThickness);
@@ -181,4 +155,4 @@ static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double
return gp;
}
}
-}
+}
\ No newline at end of file
diff --git a/Source/SVGImage/SVG/TextStyle.cs b/Source/SVGImage/SVG/TextStyle.cs
index e56de7f..f343e14 100644
--- a/Source/SVGImage/SVG/TextStyle.cs
+++ b/Source/SVGImage/SVG/TextStyle.cs
@@ -2,52 +2,153 @@
namespace SVGImage.SVG
{
+ using global::SVGImage.SVG.Utils;
using Shapes;
-
+ using System;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text.RegularExpressions;
+ using System.Windows.Media;
public sealed class TextStyle
{
+ private static readonly FontResolver _fontResolver = new FontResolver(0);
+ //This should be configurable in some way.
+ private static readonly TextStyle _defaults = new TextStyle()
+ {
+ FontFamily = "Arial Unicode MS, Verdana",
+ FontSize = 12,
+ Fontweight = FontWeights.Normal,
+ Fontstyle = FontStyles.Normal,
+ TextAlignment = System.Windows.TextAlignment.Left,
+ WordSpacing = 0,
+ LetterSpacing = 0,
+ BaseLineShift = string.Empty,
+ };
+ private string _fontFamily;
+ private double? _fontSize;
+ private FontWeight? _fontweight;
+ private FontStyle? _fontstyle;
+ private TextAlignment? _textAlignment;
+ private double? _wordSpacing;
+ private double? _letterSpacing;
+ private string _baseLineShift;
+
public TextStyle(TextStyle aCopy)
{
this.Copy(aCopy);
}
- public TextStyle(SVG svg, Shape owner)
+ private TextStyle()
+ {
+
+ }
+
+ public TextStyle(Shape owner)
{
- this.FontFamily = "Arial Unicode MS, Verdana";
- this.FontSize = 12;
- this.Fontweight = FontWeights.Normal;
- this.Fontstyle = FontStyles.Normal;
- this.TextAlignment = System.Windows.TextAlignment.Left;
- this.WordSpacing = 0;
- this.LetterSpacing = 0;
- this.BaseLineShift = string.Empty;
if (owner.Parent != null)
+ {
this.Copy(owner.Parent.TextStyle);
+ }
+ }
+
+ public Typeface GetTypeface()
+ {
+ var fontFamily = _fontResolver.ResolveFontFamily(FontFamily);
+ return new Typeface(fontFamily,
+ Fontstyle,
+ Fontweight,
+ FontStretch.FromOpenTypeStretch(9),
+ new FontFamily(_defaults.FontFamily));
}
- public string FontFamily {get; set;}
- public double FontSize {get; set;}
- public FontWeight Fontweight {get; set;}
- public FontStyle Fontstyle {get; set;}
- public TextDecorationCollection TextDecoration {get; set;}
- public TextAlignment TextAlignment {get; set;}
- public double WordSpacing {get; set;}
- public double LetterSpacing {get; set;}
- public string BaseLineShift {get; set;}
-
- public void Copy(TextStyle aCopy)
+ public GlyphTypeface GetGlyphTypeface()
+ {
+ var typeface = GetTypeface();
+ GlyphTypeface glyphTypeface;
+ if (typeface.TryGetGlyphTypeface(out glyphTypeface))
+ {
+ return glyphTypeface;
+ }
+ return null;
+ }
+
+ public string FontFamily { get => _fontFamily ?? _defaults.FontFamily; set => _fontFamily = value; }
+ public double FontSize { get => _fontSize ?? _defaults.FontSize; set => _fontSize = value; }
+ public FontWeight Fontweight { get => _fontweight ?? _defaults.Fontweight; set => _fontweight = value; }
+ public FontStyle Fontstyle { get => _fontstyle ?? _defaults.Fontstyle; set => _fontstyle = value; }
+ public TextDecorationCollection TextDecoration { get; set; }
+ public TextAlignment TextAlignment { get => _textAlignment ?? _defaults.TextAlignment; set => _textAlignment = value; }
+ public double WordSpacing { get => _wordSpacing ?? _defaults.WordSpacing; set => _wordSpacing = value; }
+ public double LetterSpacing { get => _letterSpacing ?? _defaults.LetterSpacing; set => _letterSpacing = value; }
+ public string BaseLineShift { get => _baseLineShift ?? _defaults.BaseLineShift; set => _baseLineShift = value; }
+
+ private void Copy(TextStyle aCopy)
{
if (aCopy == null)
return;
- this.FontFamily = aCopy.FontFamily;
- this.FontSize = aCopy.FontSize;
- this.Fontweight = aCopy.Fontweight;
- this.Fontstyle = aCopy.Fontstyle;;
- this.TextAlignment = aCopy.TextAlignment;
- this.WordSpacing = aCopy.WordSpacing;
- this.LetterSpacing = aCopy.LetterSpacing;
- this.BaseLineShift = aCopy.BaseLineShift;
- }
-
+ this._fontFamily = aCopy._fontFamily;
+ this._fontSize = aCopy._fontSize;
+ this._fontweight = aCopy._fontweight;
+ this._fontstyle = aCopy._fontstyle;
+ this._textAlignment = aCopy._textAlignment;
+ this._wordSpacing = aCopy._wordSpacing;
+ this._letterSpacing = aCopy._letterSpacing;
+ this._baseLineShift = aCopy._baseLineShift;
+ }
+
+ public static TextStyle Merge(TextStyle baseStyle, TextStyle overrides)
+ {
+ var result = new TextStyle();
+ result._fontFamily = overrides._fontFamily ?? baseStyle._fontFamily;
+ result._fontSize = overrides._fontSize ?? baseStyle._fontSize;
+ result._fontweight = overrides._fontweight ?? baseStyle._fontweight;
+ result._fontstyle = overrides._fontstyle ?? baseStyle._fontstyle;
+ result._textAlignment = overrides._textAlignment ?? baseStyle._textAlignment;
+ result._wordSpacing = overrides._wordSpacing ?? baseStyle._wordSpacing;
+ result._letterSpacing = overrides._letterSpacing ?? baseStyle._letterSpacing;
+ result._baseLineShift = overrides._baseLineShift ?? baseStyle._baseLineShift;
+ if (overrides.TextDecoration != null)
+ {
+ //None was explicitly set
+ if (overrides.TextDecoration.Count <= 0)
+ {
+ result.TextDecoration = new TextDecorationCollection();
+ }
+ //Copy overrides
+ else if (baseStyle.TextDecoration == null || baseStyle.TextDecoration.Count <= 0)
+ {
+ result.TextDecoration = new TextDecorationCollection(overrides.TextDecoration.Select(CopyTextDecoration));
+ }
+ //merge with base style
+ else
+ {
+ var merged = new List();
+ merged.AddRange(baseStyle.TextDecoration.Select(CopyTextDecoration));
+ merged.AddRange(overrides.TextDecoration.Select(CopyTextDecoration));
+ result.TextDecoration = new TextDecorationCollection(merged);
+ }
+ }
+ else if (baseStyle.TextDecoration != null)
+ {
+ result.TextDecoration = new TextDecorationCollection(baseStyle.TextDecoration.Select(CopyTextDecoration));
+ }
+ else
+ {
+ result.TextDecoration = null;
+ }
+ return result;
+ }
+ ///
+ /// Does not clone the TextDecorationCollection, but creates a new instance with the same properties. This prevents issues with shared references in the TextDecorationCollection.
+ ///
+ /// The TextDecoration to copy.
+ ///
+ /// A new TextDecoration instance with the same properties as the original.
+ ///
+ private static TextDecoration CopyTextDecoration(TextDecoration textDecoration)
+ {
+ return new TextDecoration(textDecoration.Location, textDecoration.Pen, textDecoration.PenOffset, textDecoration.PenOffsetUnit, textDecoration.PenThicknessUnit);
+ }
}
}
diff --git a/Source/SVGImage/SVG/Utils/DpiUtil.cs b/Source/SVGImage/SVG/Utils/DpiUtil.cs
new file mode 100644
index 0000000..7bb404f
--- /dev/null
+++ b/Source/SVGImage/SVG/Utils/DpiUtil.cs
@@ -0,0 +1,49 @@
+using System.Windows;
+using System.Reflection;
+
+namespace SVGImage.SVG.Utils
+{
+
+ public static class DpiUtil
+ {
+ static DpiUtil()
+ {
+ try
+ {
+ var sysPara = typeof(SystemParameters);
+ var dpiXProperty = sysPara.GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static);
+ var dpiYProperty = sysPara.GetProperty("Dpi", BindingFlags.NonPublic | BindingFlags.Static);
+ DpiX = (int)dpiXProperty.GetValue(null, null);
+ DpiX = (int)dpiYProperty.GetValue(null, null);
+ }
+ catch
+ {
+ DpiX = 96;
+ DpiY = 96;
+ }
+#if DPI_AWARE
+ DpiScale = new DpiScale(DpiX / 96.0, DpiY / 96.0);
+#endif
+ }
+
+ public static int DpiX { get; private set; }
+ public static int DpiY { get; private set; }
+#if DPI_AWARE
+ public static DpiScale DpiScale { get; private set; }
+#endif
+ public static double PixelsPerDip => GetPixelsPerDip();
+
+ public static double GetPixelsPerDip()
+ {
+ return DpiY / 96.0;
+ }
+
+
+
+
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Utils/EnumerableExtensions.cs b/Source/SVGImage/SVG/Utils/EnumerableExtensions.cs
new file mode 100644
index 0000000..9bab667
--- /dev/null
+++ b/Source/SVGImage/SVG/Utils/EnumerableExtensions.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+
+namespace SVGImage.SVG.Utils
+{
+ internal static class EnumerableExtensions
+ {
+ public static int IndexOfFirst(this IEnumerable source, Func predicate)
+ {
+ int i = 0;
+ foreach (var item in source)
+ {
+ if (predicate(item))
+ {
+ return i;
+ }
+ i++;
+ }
+ return -1; // Not found
+ }
+ }
+
+
+
+
+}
diff --git a/Source/SVGImage/SVG/Utils/FontResolver.cs b/Source/SVGImage/SVG/Utils/FontResolver.cs
new file mode 100644
index 0000000..a64db0b
--- /dev/null
+++ b/Source/SVGImage/SVG/Utils/FontResolver.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Windows.Media;
+
+namespace SVGImage.SVG.Utils
+{
+
+ ///
+ /// A utility class that resolves font families based on requested font names.
+ ///
+ public class FontResolver
+ {
+ private readonly ConcurrentDictionary _fontCache = new ConcurrentDictionary();
+ private readonly Dictionary _availableFonts;
+ private readonly Dictionary _normalizedFontNameMap;
+
+ ///
+ /// A utility class that resolves font families based on requested font names.
+ ///
+ /// Maximum Levenshtein distance to consider a match. If set to ≤ 0, Levenshtein matching is disabled.
+ public FontResolver(int maxLevenshteinDistance = 0)
+ {
+ _availableFonts = Fonts.SystemFontFamilies
+ .Select(ff => new { NormalName = ff.Source, Family = ff })
+ .ToDictionary(x => x.NormalName, x => x.Family, StringComparer.OrdinalIgnoreCase);
+
+ _normalizedFontNameMap = _availableFonts.Keys
+ .ToDictionary(
+ name => Normalize(name),
+ name => name,
+ StringComparer.OrdinalIgnoreCase);
+ MaxLevenshteinDistance = maxLevenshteinDistance;
+ }
+ ///
+ /// Maximum Levenshtein distance to consider a match.
+ /// If set to ≤ 0, Levenshtein matching is disabled.
+ ///
+ public int MaxLevenshteinDistance { get; set; } = 0;
+
+ ///
+ /// 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.
+ ///
+ public FontFamily ResolveFontFamily(string requestedFontName)
+ {
+ if (string.IsNullOrWhiteSpace(requestedFontName))
+ {
+ throw new ArgumentException("Font name cannot be null or empty.", nameof(requestedFontName));
+ }
+
+ if (_fontCache.TryGetValue(requestedFontName, out var cachedFont))
+ {
+ return cachedFont;
+ }
+
+ // 1. Exact match
+ if (_availableFonts.TryGetValue(requestedFontName, out var exactMatch))
+ {
+ _fontCache[requestedFontName] = exactMatch;
+ return exactMatch;
+ }
+
+ // 2. Normalized match
+ string normalizedRequested = Normalize(requestedFontName);
+ if (_normalizedFontNameMap.TryGetValue(normalizedRequested, out var normalizedMatchName) &&
+ _availableFonts.TryGetValue(normalizedMatchName, out var normalizedMatch))
+ {
+ _fontCache[requestedFontName] = normalizedMatch;
+ return normalizedMatch;
+ }
+
+ // 3. Substring match
+ var substringMatch = _availableFonts
+ .FirstOrDefault(kv => Normalize(kv.Key).Contains(normalizedRequested));
+ if (substringMatch.Value != null)
+ {
+ _fontCache[requestedFontName] = substringMatch.Value;
+ return substringMatch.Value;
+ }
+
+ // 4. Levenshtein match (optional but slow)
+ if ( MaxLevenshteinDistance > 0)
+ {
+ var bestMatch = _availableFonts
+ .Select(kv => new
+ {
+ FontName = kv.Key,
+ Font = kv.Value,
+ Distance = Levenshtein(normalizedRequested, Normalize(kv.Key))
+ })
+ .OrderBy(x => x.Distance)
+ .FirstOrDefault();
+
+ if (bestMatch != null && bestMatch.Distance <= 4)
+ {
+ _fontCache[requestedFontName] = bestMatch.Font;
+ return bestMatch.Font;
+ }
+ }
+
+
+
+ // 5. No match
+ _fontCache[requestedFontName] = null;
+ return null;
+ }
+
+ ///
+ /// Matches spaces, hyphens, and underscores
+ ///
+ private static readonly Regex _normalizationRegex = new Regex(@"[\s\-_]", RegexOptions.Compiled);
+
+ ///
+ /// Remove spaces, hyphens, underscores, and make lowercase
+ ///
+ /// The font name to normalize.
+ ///
+ /// A normalized version of the font name, with spaces, hyphens, and underscores removed, and all characters in lowercase.
+ ///
+ private static string Normalize(string fontName)
+ {
+ if (fontName is null)
+ {
+ return string.Empty;
+ }
+ return _normalizationRegex.Replace(fontName, String.Empty).ToLowerInvariant();
+
+ }
+
+ private static int[,] CreateDistanceMatrix(int length1, int length2)
+ {
+ var matrix = new int[length1 + 1, length2 + 1];
+ for (int i = 0; i <= length1; i++)
+ {
+ matrix[i, 0] = i;
+ }
+ for (int j = 0; j <= length2; j++)
+ {
+ matrix[0, j] = j;
+ }
+ return matrix;
+ }
+
+ ///
+ /// Calculates the Levenshtein distance between two strings.
+ /// The Levenshtein distance is a measure of the difference between two sequences.
+ /// It is defined as the minimum number of single-character edits (insertions, deletions, or substitutions)
+ ///
+ /// The first string to compare.
+ /// The second string to compare.
+ ///
+ /// The Levenshtein distance between the two strings.
+ ///
+ private static int Levenshtein(string string1, string string2)
+ {
+ if (string1 == string2)
+ {
+ return 0;
+ }
+
+ if (string.IsNullOrEmpty(string1))
+ {
+ return string2.Length;
+ }
+
+ if (string.IsNullOrEmpty(string2))
+ {
+ return string1.Length;
+ }
+
+
+ int[,] distanceMatrix = CreateDistanceMatrix(string1.Length, string2.Length);
+
+ for (int i = 1; i <= string1.Length; i++)
+ {
+ for (int j = 1; j <= string2.Length; j++)
+ {
+ int cost = string1[i - 1] == string2[j - 1] ? 0 : 1;
+
+ distanceMatrix[i, j] = Math.Min(
+ Math.Min(distanceMatrix[i - 1, j] + 1, // deletion
+ distanceMatrix[i, j - 1] + 1), // insertion
+ distanceMatrix[i - 1, j - 1] + cost); // substitution
+ }
+ }
+
+ return distanceMatrix[string1.Length, string2.Length];
+ }
+
+ }
+
+}
diff --git a/Source/SVGImage/SVG/Utils/TextStyleStack.cs b/Source/SVGImage/SVG/Utils/TextStyleStack.cs
new file mode 100644
index 0000000..f864651
--- /dev/null
+++ b/Source/SVGImage/SVG/Utils/TextStyleStack.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+
+namespace SVGImage.SVG.Utils
+{
+ internal sealed class TextStyleStack
+ {
+ private readonly Stack _stack = new Stack();
+ internal void Push(TextStyle textStyle)
+ {
+ if (textStyle == null)
+ {
+ throw new ArgumentNullException(nameof(textStyle), $"{nameof(textStyle)} cannot be null.");
+ }
+ if (_stack.Count == 0)
+ {
+ _stack.Push(textStyle);
+ return;
+ }
+ _stack.Push(TextStyle.Merge(_stack.Peek(), textStyle));
+ }
+
+ internal TextStyle Pop()
+ {
+ return _stack.Pop();
+ }
+ internal TextStyle Peek()
+ {
+ return _stack.Peek();
+ }
+ }
+
+
+
+
+}