From 6f8d9c5502b656dd8f135070643b6c8e74aacb34 Mon Sep 17 00:00:00 2001 From: Mike Wagner Date: Sun, 20 Jul 2025 19:15:38 -0400 Subject: [PATCH 1/5] I think I got textspan offsets working --- .../SVGImage/DotNetProjects.SVGImage.csproj | 2 +- Source/SVGImage/SVG/Shapes/Text.cs | 20 +++++++++++++++++++ Source/SVGImage/SVG/TextRender.cs | 15 ++++++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Source/SVGImage/DotNetProjects.SVGImage.csproj b/Source/SVGImage/DotNetProjects.SVGImage.csproj index 69bd12b..9afbfc9 100644 --- a/Source/SVGImage/DotNetProjects.SVGImage.csproj +++ b/Source/SVGImage/DotNetProjects.SVGImage.csproj @@ -29,7 +29,7 @@ images\dotnetprojects.png svg wpf svg-icons svg-to-png svg-to-xaml svgimage svgimage-control Readme.md - true + false 5.1.0 diff --git a/Source/SVGImage/SVG/Shapes/Text.cs b/Source/SVGImage/SVG/Shapes/Text.cs index 1a3a7c8..f062ebb 100644 --- a/Source/SVGImage/SVG/Shapes/Text.cs +++ b/Source/SVGImage/SVG/Shapes/Text.cs @@ -6,6 +6,7 @@ namespace SVGImage.SVG { using Utils; using Shapes; + using System.Linq; public sealed class TextShape : Shape { @@ -90,6 +91,11 @@ public override System.Windows.Media.Transform Transform public int StartIndex {get; set;} public string Text {get; set;} public TextSpan End {get; set;} + public double? X { get; set; } + public double? Y { get; set; } + public double? DX { get; set; } + public double? DY { get; set; } + public TextSpan(SVG svg, Shape parent, string text) : base(svg, (XmlNode)null, parent) { this.ElementType = eElementType.Text; @@ -101,6 +107,20 @@ public TextSpan(SVG svg, Shape parent, eElementType eType, List attrs this.ElementType = eType; this.Text = string.Empty; this.Children = new List(); + + if (!(attrs is null)) + { + foreach (var attr in attrs) + { + switch (attr.Name) + { + case "x": this.X = Double.Parse(attr.Value); break; + case "y": this.Y = Double.Parse(attr.Value); break; + case "dx": this.DX = Double.Parse(attr.Value); break; + case "dy": this.DY = Double.Parse(attr.Value); break; + } + } + } } public override string ToString() { diff --git a/Source/SVGImage/SVG/TextRender.cs b/Source/SVGImage/SVG/TextRender.cs index 8ce868f..dd3f520 100644 --- a/Source/SVGImage/SVG/TextRender.cs +++ b/Source/SVGImage/SVG/TextRender.cs @@ -55,6 +55,17 @@ static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle, { foreach (TextSpan child in tspan.Children) { + double spanX = x; + double spanY = y; + + // Absolute positioning if defined + if (child.X.HasValue) { spanX = child.X.Value; x = spanX; } + if (child.Y.HasValue) { spanY = child.Y.Value; y = spanY; } + + // Relative positioning + if (child.DX.HasValue) { spanX += child.DX.Value; x = spanX; } + if (child.DY.HasValue) { spanY += child.DY.Value; y = spanY; } + if (child.ElementType == TextSpan.eElementType.Text) { string txt = child.Text; @@ -62,9 +73,9 @@ static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle, double baseline = y; if (child.TextStyle.BaseLineShift == "sub") - baseline += child.TextStyle.FontSize * 0.5; /* * cap height ? fontSize*/; + baseline += child.TextStyle.FontSize * 0.5; if (child.TextStyle.BaseLineShift == "super") - baseline -= tspan.TextStyle.FontSize + (child.TextStyle.FontSize * 0.25)/*font.CapsHeight * fontSize*/; + baseline -= tspan.TextStyle.FontSize + (child.TextStyle.FontSize * 0.25); Geometry gm = BuildGlyphRun(child.TextStyle, txt, x, baseline, ref totalwidth); TextRender.SetElement(gm, child); From 1c33f3307e52672c6f97ed45e19590abca11eff4 Mon Sep 17 00:00:00 2001 From: Mike Wagner Date: Sun, 27 Jul 2025 10:15:41 -0400 Subject: [PATCH 2/5] Rewrote TextRender --- Source/SVGImage/ClassDiagram1.cd | 2 + Source/SVGImage/SVG/SVGRender.cs | 7 +- Source/SVGImage/SVG/Shapes/Group.cs | 3 +- Source/SVGImage/SVG/Shapes/Shape.cs | 1600 ++++++++++++++++++++++++++- Source/SVGImage/SVG/Shapes/Text.cs | 512 +++++---- Source/SVGImage/SVG/TextRender.cs | 684 ++++++++---- Source/SVGImage/SVG/TextStyle.cs | 112 +- 7 files changed, 2417 insertions(+), 503 deletions(-) create mode 100644 Source/SVGImage/ClassDiagram1.cd diff --git a/Source/SVGImage/ClassDiagram1.cd b/Source/SVGImage/ClassDiagram1.cd new file mode 100644 index 0000000..7b89419 --- /dev/null +++ b/Source/SVGImage/ClassDiagram1.cd @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Source/SVGImage/SVG/SVGRender.cs b/Source/SVGImage/SVG/SVGRender.cs index 814d360..f3a765a 100644 --- a/Source/SVGImage/SVG/SVGRender.cs +++ b/Source/SVGImage/SVG/SVGRender.cs @@ -428,14 +428,15 @@ internal DrawingGroup LoadGroup(IList elements, Rect? viewBox, bool isSwi AddDrawingToGroup(grp, shape, i); continue; } - if (shape is TextShape textShape) + if (shape is Text textShape) { - GeometryGroup gp = TextRender.BuildTextGeometry(textShape); + TextRender2 textRender2 = new TextRender2(); + GeometryGroup gp = textRender2.BuildTextGeometry(textShape); if (gp != null) { foreach (Geometry gm in gp.Children) { - TextSpan tspan = TextRender.GetElement(gm); + TextSpan tspan = TextRender2.GetElement(gm); if (tspan != null) { var di = this.NewDrawingItem(tspan, gm); diff --git a/Source/SVGImage/SVG/Shapes/Group.cs b/Source/SVGImage/SVG/Shapes/Group.cs index f82986b..234ee81 100644 --- a/Source/SVGImage/SVG/Shapes/Group.cs +++ b/Source/SVGImage/SVG/Shapes/Group.cs @@ -88,7 +88,8 @@ 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); + retVal = new Text(svg, childnode, parent); else if (nodeName == SVGTags.sLinearGradient) { svg.PaintServers.Create(svg, childnode); diff --git a/Source/SVGImage/SVG/Shapes/Shape.cs b/Source/SVGImage/SVG/Shapes/Shape.cs index 9bc707f..ee5e696 100644 --- a/Source/SVGImage/SVG/Shapes/Shape.cs +++ b/Source/SVGImage/SVG/Shapes/Shape.cs @@ -7,8 +7,57 @@ 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 static class DpiUtil + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "Workaround")] + 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 !DOTNET40 && !DOTNET45 && !DOTNET46 + 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 !DOTNET40 && !DOTNET45 && !DOTNET46 + public static DpiScale DpiScale { get; private set; } +#endif + public static double PixelsPerDip => GetPixelsPerDip(); + + public static double GetPixelsPerDip() + { + return DpiY / 96.0; + } + + + + + } + + public class Shape : ClipArtElement { protected Fill m_fill; @@ -22,8 +71,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 +84,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); } @@ -78,11 +147,19 @@ public virtual Stroke Stroke { get { - if (this.m_stroke != null) return this.m_stroke; + if (this.m_stroke != null) + { + return this.m_stroke; + } + var parent = this.Parent; while (parent != null) { - if (this.Parent.Stroke != null) return parent.Stroke; + if (this.Parent.Stroke != null) + { + return parent.Stroke; + } + parent = parent.Parent; } return null; @@ -97,11 +174,19 @@ public virtual Fill Fill { get { - if (this.m_fill != null) return this.m_fill; + if (this.m_fill != null) + { + return this.m_fill; + } + var parent = this.Parent; while (parent != null) { - if (parent.Fill != null) return parent.Fill; + if (parent.Fill != null) + { + return parent.Fill; + } + parent = parent.Parent; } return null; @@ -116,11 +201,19 @@ 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 +237,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 +250,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 +262,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 +351,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 +372,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 +389,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 +418,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) @@ -342,10 +457,26 @@ 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; + 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; @@ -353,9 +484,21 @@ protected virtual void Parse(SVG svg, string name, string value) } 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 +523,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; } @@ -401,4 +556,1359 @@ public override string ToString() return this.GetType().Name + " (" + (Id ?? "") + ")"; } } + + 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; + } + 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.None)); + ((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.None)); + ((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(); + } + } + + /// + /// Child elements do not inherit the relative values as specified for their parent; they inherit the computed values. + /// + public enum LengthUnit + { + Unknown = -1, + /// + /// + /// + None, + /// + /// 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, + /// + /// 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, + /// + /// Relative to font size of the element + /// + em, + + /// + /// Relative to x-height of the element’s font + /// + ex, + + /// + /// 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, + + /// + /// centimeters 1cm = 96px/2.54 + /// + cm, + /// + /// millimeters 1mm = 1/10th of 1cm + /// + mm, + /// + /// quarter-millimeters 1Q = 1/40th of 1cm + /// + Q, + /// + /// inches 1in = 2.54cm = 96px + /// + @in, + /// + /// picas 1pc = 1/6th of 1in + /// + pc, + /// + /// points 1pt = 1/72nd of 1in + /// + pt, + /// + /// pixels 1px = 1/96th of 1in + /// + px, + + + + } + public enum LengthOrientation + { + None, + Horizontal, + Vertical, + } + public class LengthContext + { + public Shape Owner { get; set; } + public LengthUnit Unit { get; set; } + private static readonly Dictionary _unitMap = new Dictionary() + { + {"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.@in}, + {"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.None; + } + 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; + } + } + + 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; + 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.@in: + 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.None: + 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 PercentWidth, PercentHeight, or 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.None: + 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.None: + 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.@in: + case LengthUnit.pc: + case LengthUnit.pt: + case LengthUnit.px: + return ResolveAbsoluteValue(_value, _context); + case LengthUnit.Unknown: + case LengthUnit.None: + default: + return _value; + } + } + /// + /// + /// + /// + /// If null, units will be ignored + public LengthPercentageOrNumber(double value, LengthContext context) + { + _context = context; + _value = value; + } + 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); + } + + } + + + + + + 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 + } + + + [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 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; + } + } + 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 + } + } + + } + + public class Text : TextShapeBase + { + public Text(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) + { + } + + } + public interface ITextChild : ITextNode + { + Shape Parent { get; set; } + } + public interface ITextNode + { + CharacterLayout GetFirstCharacter(); + CharacterLayout GetLastCharacter(); + string GetText(); + int GetLength(); + CharacterLayout[] GetCharacters(); + } + + [DebuggerDisplay("{Text}")] + /// + /// Text not wrapped in a element. + /// + public class TextString : ITextChild + { + public CharacterLayout[] Characters { get; set; } + public Shape Parent { get; set; } + public int Index { get; set; } + private static readonly Regex _trimmedWhitespace = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.Singleline); + 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; + } + } + + public class TextSpan : TextShapeBase, ITextChild + { + + public TextSpan(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) + { + } + + } + + public static partial class TextRender + { + internal class TextCursor + { + public Point Position { get; set; } + public Matrix Transform { get; set; } = Matrix.Identity; + public TextCursor(Point position) + { + Position = position; + } + public void MoveTo(double x, double y) + { + Position = new Point(x, y); + } + public void Offset(double dx, double dy) + { + Position = new Point(Position.X + dx, Position.Y + dy); + } + public void Rotate(double angleDegrees) + { + Transform.RotateAt(angleDegrees, Position.X, Position.Y); + } + } + + + + } + public class TextPath : TextShapeBase, ITextChild + { + protected TextPath(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) + { + throw new NotImplementedException("TextPath is not yet implemented."); + } + } + /// + /// 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; + public bool FirstCharacterInResolvedDescendant { get; internal set; } + public bool DoesPositionX { get; internal set; } + public bool DoesPositionY { get; internal set; } + + + } + public enum WritingMode + { + Horizontal, + Vertical, + VerticalRightToLeft + } + + public 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 + } + } + + public class TextRender2 + { + private sealed class TextRenderState : IDisposable + { + private bool _disposedValue; + + public TextRenderState(Text root, WritingMode writingMode) + { + + string text = root.GetText(); + Setup(root, text, writingMode); + InitializeResolveArrays(text.Length); + } + public bool Setup(Text root, string text, WritingMode writingMode) + { + int globalIndex = 0; + SetGlobalIndicies(root, ref globalIndex); + _characters = root.GetCharacters(); + SetFlagsAndAssignInitialPositions(root, text); + if (_characters.Length == 0) + { + return false; + } + IsHorizontal = writingMode == WritingMode.Horizontal; + return true; + } + public int BidiLevel { get; private set; } = 0; + public bool IsSideways{ get; private set; } + public bool IsHorizontal{ get; private set; } + private CharacterLayout[] _characters; + private double[] _resolvedX ; + private double[] _resolvedY ; + private double[] _resolvedDx; + private double[] _resolvedDy; + private double[] _resolvedRotate; + private int[] _xBaseIndicies; + private int[] _yBaseIndicies; + 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; + } + + 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); + } + public void Resolve(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++) + { + if (index + i >= resolvedArray.Length) + { + break; + } + resolvedArray[index + i] = list[i].Value; + } + } + + for (int i = 0; i < rotate.Count; i++) + { + if (index + i >= _resolvedRotate.Length) + { + break; + } + _resolvedRotate[index + i] = rotate[i]; + } + foreach (var child in textSpan.Children.OfType()) + { + Resolve(child); + } + ApplyResolutions(); + } + 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++; + } + } + } + private void FillInGaps() + { + FillInGaps(_resolvedX, 0d, _xBaseIndicies); + FillInGaps(_resolvedY, 0d, _yBaseIndicies); + FillInGaps(_resolvedRotate, 0d); + } + 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; + } + } + } + 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(Text 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) + { + _characters = null; + _resolvedX = null; + _resolvedY = null; + _resolvedDx = null; + _resolvedDy = null; + _resolvedRotate = null; + } + + _disposedValue = true; + } + } + + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + + public static readonly 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); + } + + public GeometryGroup BuildTextGeometry(Text text, WritingMode writingMode = WritingMode.Horizontal) + { + using(TextRenderState state = new TextRenderState(text, writingMode)) + { + if (!state.Setup(text, text.GetText(), writingMode)) + { + return null; // No characters to render + } + return CreateGeometry(text, state); + } + } + private GeometryGroup CreateGeometry(Text root, TextRenderState state) + { + state.Resolve(root); + + List textStrings = new List(); + TextStyleStack textStyleStacks = new TextStyleStack(); + PopulateTextStrings(textStrings, root, textStyleStacks); + GeometryGroup geometryGroup = new GeometryGroup(); + var baselineOrigin = new Point(root.X.FirstOrDefault().Value, root.Y.FirstOrDefault().Value); + foreach (TextString textString in textStrings) + { + if(CreateRun(textString, state, ref baselineOrigin) is GlyphRun run) + { + geometryGroup.Children.Add(run.BuildGeometry()); + } + } + + geometryGroup.Transform = root.Transform; + return geometryGroup; + } + + private static GlyphRun CreateRun(TextString textString, TextRenderState state, ref Point baselineOrigin) + { + var textStyle = textString.TextStyle; + var characterInfos = textString.GetCharacters(); + if(characterInfos is null ||characterInfos.Length == 0) + { + return null; + } + var font = textStyle.GetTypeface(); + if (!font.TryGetGlyphTypeface(out var glyphFace)) + { + 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].X) + + + 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, state.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 ; + + baselineOrigin = new Point(newX, newY); + return run; + } + + + private sealed class TextStyleStack + { + private readonly Stack _stack = new Stack(); + internal void Push(TextStyle textStyle) + { + if (textStyle == null) + { + throw new ArgumentNullException(nameof(textStyle), "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(); + } + } + + private 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); + } + } + + + + + + + + + + + + } + + public 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/Text.cs b/Source/SVGImage/SVG/Shapes/Text.cs index f062ebb..d524bdd 100644 --- a/Source/SVGImage/SVG/Shapes/Text.cs +++ b/Source/SVGImage/SVG/Shapes/Text.cs @@ -1,232 +1,282 @@ -using System; -using System.Collections.Generic; -using System.Xml; - -namespace SVGImage.SVG -{ - using Utils; - using Shapes; - using System.Linq; - - public sealed class TextShape : Shape - { - private static Fill DefaultFill = null; - private static Stroke DefaultStroke = null; - - public TextShape(SVG svg, XmlNode node, Shape parent) - : base(svg, node, parent) - { - this.X = XmlUtil.AttrValue(node, "x", 0); - this.Y = XmlUtil.AttrValue(node, "y", 0); - this.Text = node.InnerText; - this.GetTextStyle(svg); - // 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 override Fill Fill - { - get - { - Fill f = base.Fill; - if (f == null) - f = DefaultFill; - return f; - } - } - - public override Stroke Stroke - { - get - { - Stroke f = base.Stroke; - if (f == null) - f = DefaultStroke; - return f; - } - } - - TextSpan ParseTSpan(SVG svg, string tspanText) - { - try - { - return TextSpan.Parse(svg, tspanText, this); - } - catch - { - return null; - } - } - } - - public class TextSpan : Shape - { - public enum eElementType - { - Tag, - Text, - } - - 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 double? X { get; set; } - public double? Y { get; set; } - public double? DX { get; set; } - public double? DY { get; set; } - - public TextSpan(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) - : base(svg, attrs, parent) - { - this.ElementType = eType; - this.Text = string.Empty; - this.Children = new List(); - - if (!(attrs is null)) - { - foreach (var attr in attrs) - { - switch (attr.Name) - { - case "x": this.X = Double.Parse(attr.Value); break; - case "y": this.Y = Double.Parse(attr.Value); break; - case "dx": this.DX = Double.Parse(attr.Value); break; - case "dy": this.DY = Double.Parse(attr.Value); break; - } - } - } - } - public override string ToString() - { - return this.Text; - } - - static TextSpan NextTag(SVG svg, TextSpan parent, string text, ref int curPos) - { - int start = text.IndexOf("<", curPos); - if (start < 0) - return null; - int end = text.IndexOf(">", start+1); - if (end < 0) - throw new Exception("Start '<' with no end '>'"); - - end++; - - string tagtext = text.Substring(start, end - start); - if (tagtext.IndexOf("<", 1) > 0) - throw new Exception(string.Format("Start '<' within tag 'tag'")); - - List attrs = new List(); - int attrstart = tagtext.IndexOf("tspan"); - if (attrstart > 0) - { - attrstart += 5; - while (attrstart < tagtext.Length-1) - attrs.Add(StyleItem.ReadNextAttr(tagtext, ref attrstart)); - } +//using System; +//using System.Collections.Generic; +//using System.Xml; + +//namespace SVGImage.SVG +//{ +// using Utils; +// using Shapes; +// using System.Linq; + +// public sealed class TextShape : Shape +// { +// private static Fill DefaultFill = null; +// private static Stroke DefaultStroke = null; + +// public TextShape(SVG svg, XmlNode node, Shape parent) +// : base(svg, node, parent) +// { +// this.X = XmlUtil.AttrValue(node, "x", 0); +// this.Y = XmlUtil.AttrValue(node, "y", 0); +// this.Text = node.InnerText; +// this.GetTextStyle(svg); +// // check for tSpan tag +// if (node.InnerXml.IndexOf("<") >= 0) +// this.TextSpan = this.ParseTSpan(svg, node); +// 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 TextSpan2 TextSpan {get; private set;} + +// public override Fill Fill +// { +// get +// { +// Fill f = base.Fill; +// if (f == null) +// f = DefaultFill; +// return f; +// } +// } + +// public override Stroke Stroke +// { +// get +// { +// Stroke f = base.Stroke; +// if (f == null) +// f = DefaultStroke; +// return f; +// } +// } + +// TextSpan2 ParseTSpan(SVG svg, XmlNode node) +// { +// try +// { +// return TextSpan2.Parse(svg, node, this); +// } +// catch +// { +// return null; +// } +// } +// } + +// public class TextSpan2 : Shape +// { +// public enum eElementType +// { +// Tag, +// Text, +// } + +// 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 TextSpan2 End {get; set;} +// public List XList { get; set; } = new List(); +// public List YList { get; set; } = new List(); +// public List DXList { get; set; } = new List(); +// public List DYList { get; set; } = new List(); + +// public TextSpan2(SVG svg, Shape parent, string text) : base(svg, (XmlNode)null, parent) +// { +// this.ElementType = eElementType.Text; +// this.Text = text; +// } +// private List ParseNumberList(string value) +// { +// return value.Split(new[] { ' ', ',', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).Select(Double.Parse).ToList(); +// } +// 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(); +// if (!(attrs is null)) +// { +// foreach (var attr in attrs) +// { +// switch (attr.Name) +// { +// case "x": +// XList = ParseNumberList(attr.Value); +// break; +// case "y": +// YList = ParseNumberList(attr.Value); +// break; +// case "dx": +// DXList = ParseNumberList(attr.Value); +// break; +// case "dy": +// DYList = ParseNumberList(attr.Value); +// break; +// } +// } +// } +// } +// public override string ToString() +// { +// return this.Text; +// } + +// 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); +// if (end < 0) +// throw new Exception("Start '<' with no end '>'"); + +// end++; + +// string tagtext = text.Substring(start, end - start); +// if (tagtext.IndexOf("<", 1) > 0) +// throw new Exception(string.Format("Start '<' within tag 'tag'")); + +// List attrs = new List(); +// int attrstart = tagtext.IndexOf("tspan"); +// if (attrstart > 0) +// { +// attrstart += 5; +// while (attrstart < tagtext.Length-1) +// attrs.Add(StyleItem.ReadNextAttr(tagtext, ref attrstart)); +// } - TextSpan tag = new TextSpan(svg, parent, eElementType.Tag, attrs); - tag.StartIndex = start; - tag.Text = text.Substring(start, end - start); - if (tag.Text.IndexOf("<", 1) > 0) - throw new Exception(string.Format("Start '<' within tag 'tag'")); - - curPos = end; - return tag; - } - - static TextSpan Parse(SVG svg, string text, ref int curPos, TextSpan parent, TextSpan curTag) - { - TextSpan 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); - 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)); - return tag; - } - if (next != null && next.StartIndex-prevPos > 0) - { - // pure text between tspan elements - int diff = next.StartIndex-prevPos; - string s = text.Substring(prevPos, diff); - tag.Children.Add(new TextSpan(svg, tag, s)); - } - if (next.Text.StartsWith(" 0) +// throw new Exception(string.Format("Start '<' within tag 'tag'")); + +// curPos = end; +// return tag; +// } + +// static TextSpan2 Parse(SVG svg, string text, ref int curPos, TextSpan2 parent, TextSpan2 curTag) +// { +// TextSpan2 tag = curTag; +// if (tag == null) +// tag = NextTag(svg, parent, text, ref curPos); +// while (curPos < text.Length) +// { +// int prevPos = 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 TextSpan2(svg, tag, s)); +// return tag; +// } +// if (next != null && next.StartIndex-prevPos > 0) +// { +// // pure text between tspan elements +// int diff = next.StartIndex-prevPos; +// string s = text.Substring(prevPos, diff); +// tag.Children.Add(new TextSpan2(svg, tag, s)); +// } +// if (next.Text.StartsWith("() +// }; + +// foreach (XmlNode child in node.ChildNodes) +// { +// ParseXmlNode(svg, rootSpan, child); +// } + +// return rootSpan; +// } + +// private static void ParseXmlNode(SVG svg, TextSpan2 parent, XmlNode node) +// { +// if (node.NodeType == XmlNodeType.Text || node.NodeType == XmlNodeType.CDATA) +// { +// parent.Children.Add(new TextSpan2(svg, parent, node.InnerText)); +// } +// else if (node.NodeType == XmlNodeType.Element && node.Name == "tspan") +// { +// var attrs = new List(); +// foreach (XmlAttribute attr in node.Attributes) +// { +// attrs.Add(new StyleItem(attr.Name, attr.Value)); +// } + +// var tspan = new TextSpan2(svg, parent, eElementType.Tag, attrs) +// { +// Text = node.OuterXml, +// StartIndex = 0 +// }; + +// foreach (XmlNode inner in node.ChildNodes) +// { +// ParseXmlNode(svg, tspan, inner); +// } + +// parent.Children.Add(tspan); +// } +// // optionally handle other text-related tags like here +// } + + +// public static void Print(TextSpan2 tag, string indent) +// { +// if (tag.ElementType == eElementType.Text) +// Console.WriteLine("{0} '{1}'", indent, tag.Text); +// indent += " "; +// foreach (TextSpan2 c in tag.Children) +// Print(c, indent); +// } +// } + +//} diff --git a/Source/SVGImage/SVG/TextRender.cs b/Source/SVGImage/SVG/TextRender.cs index dd3f520..06fc482 100644 --- a/Source/SVGImage/SVG/TextRender.cs +++ b/Source/SVGImage/SVG/TextRender.cs @@ -1,195 +1,489 @@ -using System.Collections.Generic; -using System.Windows.Media; -using System.Windows; -using System.Reflection; - -namespace SVGImage.SVG -{ - static class TextRender - { - private static int dpiX = 0; - private static int dpiY = 0; - - static public GeometryGroup BuildTextGeometry(TextShape shape) - { - return BuildGlyphRun(shape, 0, 0); - } - - // Use GlyphRun to build the text. This allows us to define letter and word spacing - // http://books.google.com/books?id=558i6t1dKEAC&pg=PA485&source=gbs_toc_r&cad=4#v=onepage&q&f=false - 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; - GeometryGroup gp = new GeometryGroup(); - BuildTextSpan(gp, shape.TextStyle, shape.TextSpan, 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) - { - foreach (TextSpan child in tspan.Children) - { - double spanX = x; - double spanY = y; - - // Absolute positioning if defined - if (child.X.HasValue) { spanX = child.X.Value; x = spanX; } - if (child.Y.HasValue) { spanY = child.Y.Value; y = spanY; } - - // Relative positioning - if (child.DX.HasValue) { spanX += child.DX.Value; x = spanX; } - if (child.DY.HasValue) { spanY += child.DY.Value; y = spanY; } - - if (child.ElementType == TextSpan.eElementType.Text) - { - string txt = child.Text; - double totalwidth = 0; - double baseline = y; - - if (child.TextStyle.BaseLineShift == "sub") - baseline += child.TextStyle.FontSize * 0.5; - if (child.TextStyle.BaseLineShift == "super") - baseline -= tspan.TextStyle.FontSize + (child.TextStyle.FontSize * 0.25); - - Geometry gm = BuildGlyphRun(child.TextStyle, txt, x, baseline, ref totalwidth); - TextRender.SetElement(gm, child); - gp.Children.Add(gm); - x += totalwidth; - continue; - } - if (child.ElementType == TextSpan.eElementType.Tag) - BuildTextSpan(gp, textStyle, child, ref x, ref y); - } - } - - static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double y, ref double totalwidth) - { - 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, - textStyle.Fontweight, - FontStretch.FromOpenTypeStretch(9), - new FontFamily("Arial Unicode MS")); - GlyphTypeface glyphFace; - double baseline = y; - if (font.TryGetGlyphTypeface(out glyphFace)) - { -#if DOTNET40 || DOTNET45 || DOTNET46 - glyphs = new GlyphRun(); -#else - var dpiScale = new DpiScale(dpiX, dpiY); - - glyphs = new GlyphRun((float)dpiScale.PixelsPerDip); -#endif - ((System.ComponentModel.ISupportInitialize)glyphs).BeginInit(); - glyphs.GlyphTypeface = glyphFace; - glyphs.FontRenderingEmSize = fontSize; - List textChars = new List(); - List glyphIndices = new List(); - List advanceWidths = new List(); - totalwidth = 0; - char[] charsToSkip = new char[] {'\t', '\r', '\n'}; - for (int i = 0; i < text.Length; ++i) - { - char textchar = text[i]; - int codepoint = textchar; - //if (charsToSkip.Any(item => item == codepoint)) - // continue; - ushort glyphIndex; - if (glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out glyphIndex) == false) - 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]; - } - glyphs.Characters = textChars.ToArray(); - glyphs.GlyphIndices = glyphIndices.ToArray(); - glyphs.AdvanceWidths = advanceWidths.ToArray(); - - // calculate text alignment - double alignmentoffset = 0; - if (textStyle.TextAlignment == TextAlignment.Center) - alignmentoffset = totalwidth / 2; - if (textStyle.TextAlignment == TextAlignment.Right) - alignmentoffset = totalwidth; - - baseline = y; - glyphs.BaselineOrigin = new Point(x - alignmentoffset, baseline); - ((System.ComponentModel.ISupportInitialize)glyphs).EndInit(); - } - else - return new GeometryGroup(); - - // add text decoration to geometry - GeometryGroup gp = new GeometryGroup(); - gp.Children.Add(glyphs.BuildGeometry()); - if (textStyle.TextDecoration != null) - { - double decorationPos = 0; - double decorationThickness = 0; - if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Strikethrough) - { - decorationPos = baseline - (font.StrikethroughPosition * fontSize); - decorationThickness = font.StrikethroughThickness * fontSize; - } - if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Underline) - { - decorationPos = baseline - (font.UnderlinePosition * fontSize); - decorationThickness = font.UnderlineThickness * fontSize; - } - if (textStyle.TextDecoration[0].Location == TextDecorationLocation.OverLine) - { - decorationPos = baseline - fontSize; - decorationThickness = font.StrikethroughThickness * fontSize; - } - Rect bounds = new Rect(gp.Bounds.Left, decorationPos, gp.Bounds.Width, decorationThickness); - gp.Children.Add(new RectangleGeometry(bounds)); - - } - return gp; - } - } -} +//using System.Collections.Generic; +//using System.Windows.Media; +//using System.Windows; +//using System.Reflection; +//using System.Windows.Media.TextFormatting; + +//namespace SVGImage.SVG +//{ +// static class TextRender +// { +// private static int dpiX = 0; +// private static int dpiY = 0; + +// static public GeometryGroup BuildTextGeometry(TextShape shape) +// { +// double cursorX = 0; +// double cursorY = 0; +// return BuildTextSpan(shape.TextSpan, shape.TextStyle, ref cursorX, ref cursorY); +// //return BuildGlyphRun(shape, 0, 0); +// } + +// // Use GlyphRun to build the text. This allows us to define letter and word spacing +// // http://books.google.com/books?id=558i6t1dKEAC&pg=PA485&source=gbs_toc_r&cad=4#v=onepage&q&f=false +// static GeometryGroup BuildGlyphRun(TextShape shape) +// { +// 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, +// shape.TextSpan.XList, +// shape.TextSpan.YList, +// shape.TextSpan.DXList, +// shape.TextSpan.DYList)); +// return gp; +// } +// double cursorX = 0; +// double cursorY = 0; +// return BuildTextSpan(shape.TextSpan, shape.TextStyle, ref cursorX, ref cursorY); +// } + +// //static GeometryGroup BuildTextSpan(TextShape shape) +// //{ +// // double x = shape.X; +// // double y = shape.Y; +// // GeometryGroup gp = new GeometryGroup(); +// // BuildTextSpan(gp, shape.TextStyle, shape.TextSpan, ref x, ref y); +// // return gp; +// //} + +// private static GeometryGroup BuildTextSpan(TextSpan tspan, TextStyle style, ref double cursorX, ref double cursorY) +// { +// GeometryGroup group = new GeometryGroup(); +// double localX = cursorX; +// double localY = cursorY; + +// foreach (TextSpan child in tspan.Children) +// { +// double spanX = localX; +// double spanY = localY; + +// // Apply absolute positioning if x/y exist +// if (child.XList.Count > 0) +// { +// spanX = child.XList[0]; +// } +// else if (child.DXList.Count > 0) +// { +// spanX += child.DXList[0]; +// } + +// if (child.YList.Count > 0) +// { +// spanY = child.YList[0]; +// } +// else if (child.DYList.Count > 0) +// { +// spanY += child.DYList[0]; +// } + +// double totalWidth = 0.0; + +// if (child.ElementType == TextSpan.eElementType.Text) +// { +// Geometry gm = BuildGlyphRun( +// child.TextStyle ?? style, +// child.Text, +// spanX, +// spanY, +// ref totalWidth, +// child.XList, +// child.YList, +// child.DXList, +// child.DYList +// ); + +// group.Children.Add(gm); +// } +// else +// { +// Geometry gm = BuildTextSpan(child, child.TextStyle ?? style, ref spanX, ref spanY); +// group.Children.Add(gm); +// } + +// // Advance cursor only if this tspan didn't reset x +// if (child.XList.Count == 0) +// { +// localX = spanX + totalWidth; +// } +// else +// { +// localX = spanX; // reset after absolute x +// } + +// if (child.YList.Count == 0) +// { +// localY = spanY; +// } +// else +// { +// localY = spanY; +// } +// } + +// cursorX = localX; +// cursorY = localY; +// return group; +// } + +// 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) +// { +// //int xi = 0, yi = 0, dxi = 0, dyi = 0; +// //BuildTextSpan(gp, textStyle, tspan, ref x, ref y, ref xi, ref yi, ref dxi, ref dyi); +// var builtSpan = BuildTextSpan(tspan, textStyle, ref x, ref y); +// gp.Children.Add(builtSpan); + +// } + + + + + +// //static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle, +// // TextSpan tspan, ref double x, ref double y, +// // ref int xIndex, ref int yIndex, ref int dxIndex, ref int dyIndex) +// //{ +// // foreach (TextSpan child in tspan.Children) +// // { +// // double spanX = x; +// // double spanY = y; + +// // if (child.XList.Count > xIndex) { spanX = child.XList[xIndex++]; x = spanX; } +// // if (child.YList.Count > yIndex) { spanY = child.YList[yIndex++]; y = spanY; } + +// // if (child.DXList.Count > dxIndex) { spanX += child.DXList[dxIndex++]; x = spanX; } +// // if (child.DYList.Count > dyIndex) { spanY += child.DYList[dyIndex++]; y = spanY; } + +// // if (child.ElementType == TextSpan.eElementType.Text) +// // { +// // // Inherit position attributes from parent if not defined +// // List xList = (child.XList.Count > 0) ? child.XList : tspan.XList; +// // List yList = (child.YList.Count > 0) ? child.YList : tspan.YList; +// // List dxList = (child.DXList.Count > 0) ? child.DXList : tspan.DXList; +// // List dyList = (child.DYList.Count > 0) ? child.DYList : tspan.DYList; +// // string txt = child.Text; +// // double totalwidth = 0; +// // double baseline = y; + +// // if (child.TextStyle.BaseLineShift == "sub") +// // baseline += child.TextStyle.FontSize * 0.5; +// // if (child.TextStyle.BaseLineShift == "super") +// // baseline -= tspan.TextStyle.FontSize + (child.TextStyle.FontSize * 0.25); + +// // Geometry gm = BuildGlyphRun(child.TextStyle, txt, spanX, baseline, ref totalwidth, +// // xList, yList, dxList, dyList); +// // TextRender.SetElement(gm, child); +// // gp.Children.Add(gm); +// // x += totalwidth; +// // } +// // else if (child.ElementType == TextSpan.eElementType.Tag) +// // { +// // BuildTextSpan(gp, textStyle, child, ref x, ref y, ref xIndex, ref yIndex, ref dxIndex, ref dyIndex); +// // } +// // } +// //} + +// static Geometry BuildGlyphRun( +// TextStyle textStyle, +// string text, +// double x, +// double y, +// ref double totalWidth, +// List xList, +// List yList, +// List dxList, +// List dyList) +// { +// if (string.IsNullOrEmpty(text)) +// { +// return Geometry.Empty; +// } + +// 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; +// Typeface font = new Typeface(new FontFamily(textStyle.FontFamily), +// textStyle.Fontstyle, +// textStyle.Fontweight, +// FontStretch.FromOpenTypeStretch(9), +// new FontFamily("Arial Unicode MS")); +// GlyphTypeface glyphFace; + +// if (!font.TryGetGlyphTypeface(out glyphFace)) +// { +// return new GeometryGroup(); +// } + + +// List textChars = new List(); +// List glyphIndices = new List(); +// List advanceWidths = new List(); + +// for (int i = 0; i < text.Length; i++) +// { +// char textchar = text[i]; +// int codepoint = textchar; +// if (!glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out var glyphIndex)) +// { +// glyphIndices[i] = 0; // fallback to default glyph +// } +// glyphIndices.Add(glyphIndex); + +// double glyphWidth = glyphFace.AdvanceWidths[glyphIndex] * fontSize + textStyle.LetterSpacing; +// if (char.IsWhiteSpace(textchar)) +// glyphWidth += textStyle.WordSpacing; + +// textChars.Add(textchar); +// advanceWidths.Add(glyphWidth); +// } + +// Point baseline = new Point(x, y); +// List origins = new List(); +// double currentX = x; +// double currentY = y; + +// for (int i = 0; i < text.Length; i++) +// { +// if (xList != null && i < xList.Count) +// { +// currentX = xList[i]; +// } +// else if (dxList != null && i < dxList.Count) +// { +// currentX += dxList[i]; +// } + +// if (yList != null && i < yList.Count) +// { +// currentY = yList[i]; +// } +// else if (dyList != null && i < dyList.Count) +// { +// currentY += dyList[i]; +// } + +// origins.Add(new Point(currentX, -currentY)); +// currentX += advanceWidths[i]; +// } + +// totalWidth = currentX - x; + +// GlyphRun glyphs = new GlyphRun( +// glyphFace, +// 0, +// false, +// fontSize, +//#if !(DOTNET40 || DOTNET45 || DOTNET46) +// (float)(new DpiScale(dpiX, dpiY)).PixelsPerDip, +//#endif +// glyphIndices, +// new Point(baseline.X - 50, baseline.Y + 20), +// advanceWidths, +// origins, +// null, +// null, +// null, +// null, +// null +// ); + + + +// // add text decoration to geometry +// GeometryGroup gp = new GeometryGroup(); +// gp.Children.Add(glyphs.BuildGeometry()); +// if (textStyle.TextDecoration != null) +// { +// double decorationPos = 0; +// double decorationThickness = 0; +// if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Strikethrough) +// { +// decorationPos = baseline.Y - (font.StrikethroughPosition * fontSize); +// decorationThickness = font.StrikethroughThickness * fontSize; +// } +// if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Underline) +// { +// decorationPos = baseline.Y - (font.UnderlinePosition * fontSize); +// decorationThickness = font.UnderlineThickness * fontSize; +// } +// if (textStyle.TextDecoration[0].Location == TextDecorationLocation.OverLine) +// { +// decorationPos = baseline.Y - fontSize; +// decorationThickness = font.StrikethroughThickness * fontSize; +// } +// Rect bounds = new Rect(gp.Bounds.Left, decorationPos, gp.Bounds.Width, decorationThickness); +// gp.Children.Add(new RectangleGeometry(bounds)); + +// } +// return gp; +// } + + +// // static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double y, ref double totalwidth, +// // List xList = null, +// // List yList = null, +// // List dxList = null, +// // List dyList = null) +// // { +// // 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, +// // textStyle.Fontweight, +// // FontStretch.FromOpenTypeStretch(9), +// // new FontFamily("Arial Unicode MS")); +// // GlyphTypeface glyphFace; +// // double baseline = y; +// // if (!font.TryGetGlyphTypeface(out glyphFace)) +// // { +// // return new GeometryGroup(); +// // } +// //#if DOTNET40 || DOTNET45 || DOTNET46 +// // glyphs = new GlyphRun(); +// //#else +// // var dpiScale = new DpiScale(dpiX, dpiY); + +// // glyphs = new GlyphRun((float)dpiScale.PixelsPerDip); +// //#endif +// // ((System.ComponentModel.ISupportInitialize)glyphs).BeginInit(); +// // glyphs.GlyphTypeface = glyphFace; +// // glyphs.FontRenderingEmSize = fontSize; +// // List textChars = new List(); +// // List glyphIndices = new List(); +// // List advanceWidths = new List(); +// // totalwidth = 0; +// // char[] charsToSkip = new char[] { '\t', '\r', '\n' }; +// // List glyphOffsets = new List(); + +// // double currentX = x; +// // double currentY = y; +// // for (int i = 0; i < text.Length; ++i) +// // { +// // char textchar = text[i]; +// // int codepoint = textchar; + +// // if (!glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out ushort glyphIndex)) +// // continue; + +// // textChars.Add(textchar); + +// // double glyphWidth = glyphFace.AdvanceWidths[glyphIndex] * fontSize + textStyle.LetterSpacing; +// // if (char.IsWhiteSpace(textchar)) +// // glyphWidth += textStyle.WordSpacing; + +// // // Absolute overrides (apply once, not cumulative) +// // if (!(xList is null) && i < xList.Count) +// // currentX = xList[i]; +// // if (!(yList is null) && i < yList.Count) +// // currentY = yList[i]; + +// // // Relative offsets (cumulative) +// // if (!(dxList is null) && i < dxList.Count) +// // currentX += dxList[i]; +// // if (!(dyList is null) && i < dyList.Count) +// // currentY += dyList[i]; + +// // glyphIndices.Add(glyphIndex); +// // advanceWidths.Add(0); // Width will be zero since position is handled by offset +// // glyphOffsets.Add(new Point(currentX, -currentY)); + +// // currentX += glyphWidth; +// // totalwidth += glyphWidth; + + + +// // //char textchar = text[i]; +// // //int codepoint = textchar; +// // ////if (charsToSkip.Any(item => item == codepoint)) +// // //// continue; +// // //ushort glyphIndex; +// // //if (glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out glyphIndex) == false) +// // // 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]; +// // } +// // glyphs.GlyphOffsets = glyphOffsets.ToArray(); +// // //glyphs.GlyphOffsets = new Point[] { new Point(glyphOffsets[0].X, -glyphOffsets[0].Y), new Point(glyphOffsets[1].X, -glyphOffsets[1].Y), new Point(glyphOffsets[2].X, -glyphOffsets[2].Y) }; +// // glyphs.Characters = textChars.ToArray(); +// // glyphs.GlyphIndices = glyphIndices.ToArray(); +// // glyphs.AdvanceWidths = advanceWidths.ToArray(); + +// // // calculate text alignment +// // double alignmentoffset = 0; +// // if (textStyle.TextAlignment == TextAlignment.Center) +// // alignmentoffset = totalwidth / 2; +// // if (textStyle.TextAlignment == TextAlignment.Right) +// // alignmentoffset = totalwidth; + +// // baseline = y; +// // //glyphs.BaselineOrigin = new Point(x - alignmentoffset, baseline); +// // glyphs.BaselineOrigin = new Point(0 - alignmentoffset, 0); +// // ((System.ComponentModel.ISupportInitialize)glyphs).EndInit(); + + +// // // add text decoration to geometry +// // GeometryGroup gp = new GeometryGroup(); +// // gp.Children.Add(glyphs.BuildGeometry()); +// // if (textStyle.TextDecoration != null) +// // { +// // double decorationPos = 0; +// // double decorationThickness = 0; +// // if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Strikethrough) +// // { +// // decorationPos = baseline - (font.StrikethroughPosition * fontSize); +// // decorationThickness = font.StrikethroughThickness * fontSize; +// // } +// // if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Underline) +// // { +// // decorationPos = baseline - (font.UnderlinePosition * fontSize); +// // decorationThickness = font.UnderlineThickness * fontSize; +// // } +// // if (textStyle.TextDecoration[0].Location == TextDecorationLocation.OverLine) +// // { +// // decorationPos = baseline - fontSize; +// // decorationThickness = font.StrikethroughThickness * fontSize; +// // } +// // Rect bounds = new Rect(gp.Bounds.Left, decorationPos, gp.Bounds.Width, decorationThickness); +// // gp.Children.Add(new RectangleGeometry(bounds)); + +// // } +// // return gp; +// // } +// } +//} diff --git a/Source/SVGImage/SVG/TextStyle.cs b/Source/SVGImage/SVG/TextStyle.cs index e56de7f..52022c9 100644 --- a/Source/SVGImage/SVG/TextStyle.cs +++ b/Source/SVGImage/SVG/TextStyle.cs @@ -3,51 +3,107 @@ namespace SVGImage.SVG { using Shapes; + using System.Windows.Media; public sealed class TextStyle { + //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() + { + return new Typeface(new FontFamily(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 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; set;} - public double WordSpacing {get; set;} - public double LetterSpacing {get; set;} - public string BaseLineShift {get; set;} - - public void Copy(TextStyle aCopy) + 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; + return result; + } + } + } From 383a4478f2c8414da1e0e86a6be0c95f808856cb Mon Sep 17 00:00:00 2001 From: Mike Wagner Date: Sun, 27 Jul 2025 21:19:04 -0400 Subject: [PATCH 3/5] Got text decorations somewhat working. Split up files --- Source/SVGImage/ClassDiagram1.cd | 2 - .../SVGImage/DotNetProjects.SVGImage.csproj | 12 +- Source/SVGImage/SVG/SVGImage.cs | 21 +- Source/SVGImage/SVG/SVGRender.cs | 16 +- Source/SVGImage/SVG/Shapes/CharacterLayout.cs | 48 + Source/SVGImage/SVG/Shapes/CircleShape.cs | 16 +- Source/SVGImage/SVG/Shapes/EllipseShape.cs | 16 +- Source/SVGImage/SVG/Shapes/Group.cs | 2 +- Source/SVGImage/SVG/Shapes/ITextChild.cs | 11 + Source/SVGImage/SVG/Shapes/ITextNode.cs | 15 + .../SVGImage/SVG/Shapes/LengthAdjustment.cs | 19 + Source/SVGImage/SVG/Shapes/LengthContext.cs | 65 + .../SVGImage/SVG/Shapes/LengthOrientation.cs | 13 + .../SVG/Shapes/LengthPercentageOrNumber.cs | 173 ++ .../Shapes/LengthPercentageOrNumberList.cs | 107 ++ Source/SVGImage/SVG/Shapes/LengthUnit.cs | 123 ++ Source/SVGImage/SVG/Shapes/PathShape.cs | 17 +- Source/SVGImage/SVG/Shapes/PolygonShape.cs | 15 +- Source/SVGImage/SVG/Shapes/RectangleShape.cs | 14 +- Source/SVGImage/SVG/Shapes/Shape.cs | 1508 +---------------- Source/SVGImage/SVG/Shapes/Text.cs | 470 ++--- .../SVGImage/SVG/Shapes/TextLengthResolver.cs | 175 ++ Source/SVGImage/SVG/Shapes/TextPath.cs | 17 + Source/SVGImage/SVG/Shapes/TextRender.cs | 186 ++ Source/SVGImage/SVG/Shapes/TextRenderBase.cs | 24 + Source/SVGImage/SVG/Shapes/TextRenderState.cs | 362 ++++ Source/SVGImage/SVG/Shapes/TextShape.cs | 17 + Source/SVGImage/SVG/Shapes/TextShapeBase.cs | 203 +++ Source/SVGImage/SVG/Shapes/TextSpan.cs | 17 + Source/SVGImage/SVG/Shapes/TextString.cs | 62 + Source/SVGImage/SVG/Shapes/WritingMode.cs | 41 + Source/SVGImage/SVG/TextRender.cs | 489 ------ Source/SVGImage/SVG/TextRender2.cs | 158 ++ Source/SVGImage/SVG/TextStyle.cs | 55 +- Source/SVGImage/SVG/Utils/DpiUtil.cs | 49 + .../SVG/Utils/EnumerableExtensions.cs | 26 + Source/SVGImage/SVG/Utils/FontResolver.cs | 201 +++ Source/SVGImage/SVG/Utils/TextStyleStack.cs | 36 + 38 files changed, 2491 insertions(+), 2310 deletions(-) delete mode 100644 Source/SVGImage/ClassDiagram1.cd create mode 100644 Source/SVGImage/SVG/Shapes/CharacterLayout.cs create mode 100644 Source/SVGImage/SVG/Shapes/ITextChild.cs create mode 100644 Source/SVGImage/SVG/Shapes/ITextNode.cs create mode 100644 Source/SVGImage/SVG/Shapes/LengthAdjustment.cs create mode 100644 Source/SVGImage/SVG/Shapes/LengthContext.cs create mode 100644 Source/SVGImage/SVG/Shapes/LengthOrientation.cs create mode 100644 Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs create mode 100644 Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs create mode 100644 Source/SVGImage/SVG/Shapes/LengthUnit.cs create mode 100644 Source/SVGImage/SVG/Shapes/TextLengthResolver.cs create mode 100644 Source/SVGImage/SVG/Shapes/TextPath.cs create mode 100644 Source/SVGImage/SVG/Shapes/TextRender.cs create mode 100644 Source/SVGImage/SVG/Shapes/TextRenderBase.cs create mode 100644 Source/SVGImage/SVG/Shapes/TextRenderState.cs create mode 100644 Source/SVGImage/SVG/Shapes/TextShape.cs create mode 100644 Source/SVGImage/SVG/Shapes/TextShapeBase.cs create mode 100644 Source/SVGImage/SVG/Shapes/TextSpan.cs create mode 100644 Source/SVGImage/SVG/Shapes/TextString.cs create mode 100644 Source/SVGImage/SVG/Shapes/WritingMode.cs delete mode 100644 Source/SVGImage/SVG/TextRender.cs create mode 100644 Source/SVGImage/SVG/TextRender2.cs create mode 100644 Source/SVGImage/SVG/Utils/DpiUtil.cs create mode 100644 Source/SVGImage/SVG/Utils/EnumerableExtensions.cs create mode 100644 Source/SVGImage/SVG/Utils/FontResolver.cs create mode 100644 Source/SVGImage/SVG/Utils/TextStyleStack.cs diff --git a/Source/SVGImage/ClassDiagram1.cd b/Source/SVGImage/ClassDiagram1.cd deleted file mode 100644 index 7b89419..0000000 --- a/Source/SVGImage/ClassDiagram1.cd +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Source/SVGImage/DotNetProjects.SVGImage.csproj b/Source/SVGImage/DotNetProjects.SVGImage.csproj index 9afbfc9..e704de2 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 f3a765a..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); @@ -428,16 +437,15 @@ internal DrawingGroup LoadGroup(IList elements, Rect? viewBox, bool isSwi AddDrawingToGroup(grp, shape, i); continue; } - if (shape is Text textShape) + if (shape is TextShape textShape) { - TextRender2 textRender2 = new TextRender2(); + TextRender textRender2 = new TextRender(); GeometryGroup gp = textRender2.BuildTextGeometry(textShape); if (gp != null) { foreach (Geometry gm in gp.Children) { - TextSpan tspan = TextRender2.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 234ee81..0eba3af 100644 --- a/Source/SVGImage/SVG/Shapes/Group.cs +++ b/Source/SVGImage/SVG/Shapes/Group.cs @@ -89,7 +89,7 @@ private Shape AddToList(SVG svg, XmlNode childnode, Shape parent, bool isDefinit retVal = new AnimateTransform(svg, childnode, parent); else if (nodeName == SVGTags.sText) //retVal = new TextShape(svg, childnode, parent); - retVal = new Text(svg, childnode, parent); + retVal = new TextShape(svg, childnode, parent); else if (nodeName == SVGTags.sLinearGradient) { svg.PaintServers.Create(svg, childnode); 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..0ae8898 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs @@ -0,0 +1,173 @@ +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; + 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; + } + } + /// + /// + /// + /// + /// If null, units will be ignored + public LengthPercentageOrNumber(double value, LengthContext context) + { + _context = context; + _value = value; + } + 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..3ed27e8 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs @@ -0,0 +1,107 @@ +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; + } + 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 ee5e696..7d45c29 100644 --- a/Source/SVGImage/SVG/Shapes/Shape.cs +++ b/Source/SVGImage/SVG/Shapes/Shape.cs @@ -17,49 +17,9 @@ namespace SVGImage.SVG.Shapes using System.Collections; using System.Reflection; - public static class DpiUtil - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "Workaround")] - 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 !DOTNET40 && !DOTNET45 && !DOTNET46 - 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 !DOTNET40 && !DOTNET45 && !DOTNET46 - public static DpiScale DpiScale { get; private set; } -#endif - public static double PixelsPerDip => GetPixelsPerDip(); - - public static double GetPixelsPerDip() - { - return DpiY / 96.0; - } - - - - - } - - public class Shape : ClipArtElement { + private static readonly Regex _whiteSpaceRegex = new Regex(@"\s+"); protected Fill m_fill; protected Stroke m_stroke; @@ -143,58 +103,46 @@ internal Clip Clip public Visibility Visibility { get; set; } - public virtual Stroke Stroke - { - get - { - if (this.m_stroke != null) + protected virtual Stroke DefaultStroke() + { + var parent = this.Parent; + while (parent != null) + { + if (parent.Stroke != null) { - return this.m_stroke; + return parent.Stroke; } - var parent = this.Parent; - while (parent != null) - { - if (this.Parent.Stroke != null) - { - return parent.Stroke; - } + parent = parent.Parent; + } + return null; + } - parent = parent.Parent; - } - return null; - } - set - { - m_stroke = value; - } + public Stroke Stroke + { + get => m_stroke ?? DefaultStroke(); + set => m_stroke = value; } - public virtual Fill Fill - { - get - { - if (this.m_fill != null) + protected virtual Fill DefaultFill() + { + var parent = this.Parent; + while (parent != null) + { + if (parent.Fill != null) { - return this.m_fill; + return parent.Fill; } - var parent = this.Parent; - while (parent != null) - { - if (parent.Fill != null) - { - return parent.Fill; - } + parent = parent.Parent; + } + return null; + } - parent = parent.Parent; - } - return null; - } - set - { - m_fill = value; - } + public Fill Fill + { + get => m_fill ?? DefaultFill(); + set => m_fill = value; } public virtual TextStyle TextStyle @@ -456,30 +404,39 @@ 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") + } + var textDecorations = _whiteSpaceRegex.Split(value); + TextDecorationCollection tt = new TextDecorationCollection(); + if (textDecorations.Length == 0 || textDecorations.Any(td => td.Equals("none", StringComparison.OrdinalIgnoreCase))) { - t.Location = TextDecorationLocation.OverLine; + // 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; } - - if (value == "line-through") + foreach (var textDecoration in textDecorations) { - t.Location = TextDecorationLocation.Strikethrough; + 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); } - - TextDecorationCollection tt = new TextDecorationCollection(); - tt.Add(t); - this.GetTextStyle(svg).TextDecoration = tt; + + this.GetTextStyle(svg).TextDecoration = tt; + return; } if (name == SVGTags.sTextAnchor) @@ -555,1357 +512,6 @@ public override string ToString() { return this.GetType().Name + " (" + (Id ?? "") + ")"; } - } - - 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; - } - 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.None)); - ((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.None)); - ((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(); - } - } - - /// - /// Child elements do not inherit the relative values as specified for their parent; they inherit the computed values. - /// - public enum LengthUnit - { - Unknown = -1, - /// - /// - /// - None, - /// - /// 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, - /// - /// 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, - /// - /// Relative to font size of the element - /// - em, - - /// - /// Relative to x-height of the element’s font - /// - ex, - - /// - /// 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, - - /// - /// centimeters 1cm = 96px/2.54 - /// - cm, - /// - /// millimeters 1mm = 1/10th of 1cm - /// - mm, - /// - /// quarter-millimeters 1Q = 1/40th of 1cm - /// - Q, - /// - /// inches 1in = 2.54cm = 96px - /// - @in, - /// - /// picas 1pc = 1/6th of 1in - /// - pc, - /// - /// points 1pt = 1/72nd of 1in - /// - pt, - /// - /// pixels 1px = 1/96th of 1in - /// - px, - - - - } - public enum LengthOrientation - { - None, - Horizontal, - Vertical, - } - public class LengthContext - { - public Shape Owner { get; set; } - public LengthUnit Unit { get; set; } - private static readonly Dictionary _unitMap = new Dictionary() - { - {"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.@in}, - {"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.None; - } - 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; - } - } - - 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; - 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.@in: - 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.None: - 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 PercentWidth, PercentHeight, or 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.None: - 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.None: - 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.@in: - case LengthUnit.pc: - case LengthUnit.pt: - case LengthUnit.px: - return ResolveAbsoluteValue(_value, _context); - case LengthUnit.Unknown: - case LengthUnit.None: - default: - return _value; - } - } - /// - /// - /// - /// - /// If null, units will be ignored - public LengthPercentageOrNumber(double value, LengthContext context) - { - _context = context; - _value = value; - } - 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); - } - - } - - - - - - 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 - } - - - [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 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; - } - } - 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 - } - } - - } - - public class Text : TextShapeBase - { - public Text(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) - { - } - - } - public interface ITextChild : ITextNode - { - Shape Parent { get; set; } - } - public interface ITextNode - { - CharacterLayout GetFirstCharacter(); - CharacterLayout GetLastCharacter(); - string GetText(); - int GetLength(); - CharacterLayout[] GetCharacters(); - } - - [DebuggerDisplay("{Text}")] - /// - /// Text not wrapped in a element. - /// - public class TextString : ITextChild - { - public CharacterLayout[] Characters { get; set; } - public Shape Parent { get; set; } - public int Index { get; set; } - private static readonly Regex _trimmedWhitespace = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.Singleline); - 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; - } - } - - public class TextSpan : TextShapeBase, ITextChild - { - - public TextSpan(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) - { - } - - } - - public static partial class TextRender - { - internal class TextCursor - { - public Point Position { get; set; } - public Matrix Transform { get; set; } = Matrix.Identity; - public TextCursor(Point position) - { - Position = position; - } - public void MoveTo(double x, double y) - { - Position = new Point(x, y); - } - public void Offset(double dx, double dy) - { - Position = new Point(Position.X + dx, Position.Y + dy); - } - public void Rotate(double angleDegrees) - { - Transform.RotateAt(angleDegrees, Position.X, Position.Y); - } - } - - - - } - public class TextPath : TextShapeBase, ITextChild - { - protected TextPath(SVG svg, XmlNode node, Shape parent) : base(svg, node, parent) - { - throw new NotImplementedException("TextPath is not yet implemented."); - } - } - /// - /// 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; - public bool FirstCharacterInResolvedDescendant { get; internal set; } - public bool DoesPositionX { get; internal set; } - public bool DoesPositionY { get; internal set; } - - - } - public enum WritingMode - { - Horizontal, - Vertical, - VerticalRightToLeft - } - - public 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 - } - } - - public class TextRender2 - { - private sealed class TextRenderState : IDisposable - { - private bool _disposedValue; - - public TextRenderState(Text root, WritingMode writingMode) - { - - string text = root.GetText(); - Setup(root, text, writingMode); - InitializeResolveArrays(text.Length); - } - public bool Setup(Text root, string text, WritingMode writingMode) - { - int globalIndex = 0; - SetGlobalIndicies(root, ref globalIndex); - _characters = root.GetCharacters(); - SetFlagsAndAssignInitialPositions(root, text); - if (_characters.Length == 0) - { - return false; - } - IsHorizontal = writingMode == WritingMode.Horizontal; - return true; - } - public int BidiLevel { get; private set; } = 0; - public bool IsSideways{ get; private set; } - public bool IsHorizontal{ get; private set; } - private CharacterLayout[] _characters; - private double[] _resolvedX ; - private double[] _resolvedY ; - private double[] _resolvedDx; - private double[] _resolvedDy; - private double[] _resolvedRotate; - private int[] _xBaseIndicies; - private int[] _yBaseIndicies; - 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; - } - - 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); - } - public void Resolve(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++) - { - if (index + i >= resolvedArray.Length) - { - break; - } - resolvedArray[index + i] = list[i].Value; - } - } - - for (int i = 0; i < rotate.Count; i++) - { - if (index + i >= _resolvedRotate.Length) - { - break; - } - _resolvedRotate[index + i] = rotate[i]; - } - foreach (var child in textSpan.Children.OfType()) - { - Resolve(child); - } - ApplyResolutions(); - } - 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++; - } - } - } - private void FillInGaps() - { - FillInGaps(_resolvedX, 0d, _xBaseIndicies); - FillInGaps(_resolvedY, 0d, _yBaseIndicies); - FillInGaps(_resolvedRotate, 0d); - } - 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; - } - } - } - 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(Text 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) - { - _characters = null; - _resolvedX = null; - _resolvedY = null; - _resolvedDx = null; - _resolvedDy = null; - _resolvedRotate = null; - } - - _disposedValue = true; - } - } - - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - } - - public static readonly 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); - } - - public GeometryGroup BuildTextGeometry(Text text, WritingMode writingMode = WritingMode.Horizontal) - { - using(TextRenderState state = new TextRenderState(text, writingMode)) - { - if (!state.Setup(text, text.GetText(), writingMode)) - { - return null; // No characters to render - } - return CreateGeometry(text, state); - } - } - private GeometryGroup CreateGeometry(Text root, TextRenderState state) - { - state.Resolve(root); - - List textStrings = new List(); - TextStyleStack textStyleStacks = new TextStyleStack(); - PopulateTextStrings(textStrings, root, textStyleStacks); - GeometryGroup geometryGroup = new GeometryGroup(); - var baselineOrigin = new Point(root.X.FirstOrDefault().Value, root.Y.FirstOrDefault().Value); - foreach (TextString textString in textStrings) - { - if(CreateRun(textString, state, ref baselineOrigin) is GlyphRun run) - { - geometryGroup.Children.Add(run.BuildGeometry()); - } - } - - geometryGroup.Transform = root.Transform; - return geometryGroup; - } - - private static GlyphRun CreateRun(TextString textString, TextRenderState state, ref Point baselineOrigin) - { - var textStyle = textString.TextStyle; - var characterInfos = textString.GetCharacters(); - if(characterInfos is null ||characterInfos.Length == 0) - { - return null; - } - var font = textStyle.GetTypeface(); - if (!font.TryGetGlyphTypeface(out var glyphFace)) - { - 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].X) - - - 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, state.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 ; - - baselineOrigin = new Point(newX, newY); - return run; - } - - - private sealed class TextStyleStack - { - private readonly Stack _stack = new Stack(); - internal void Push(TextStyle textStyle) - { - if (textStyle == null) - { - throw new ArgumentNullException(nameof(textStyle), "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(); - } - } - - private 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); - } - } - - - - - - - - - - - - } - - public 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/Text.cs b/Source/SVGImage/SVG/Shapes/Text.cs index d524bdd..e51344e 100644 --- a/Source/SVGImage/SVG/Shapes/Text.cs +++ b/Source/SVGImage/SVG/Shapes/Text.cs @@ -1,282 +1,188 @@ -//using System; -//using System.Collections.Generic; -//using System.Xml; - -//namespace SVGImage.SVG -//{ -// using Utils; -// using Shapes; -// using System.Linq; - -// public sealed class TextShape : Shape -// { -// private static Fill DefaultFill = null; -// private static Stroke DefaultStroke = null; - -// public TextShape(SVG svg, XmlNode node, Shape parent) -// : base(svg, node, parent) -// { -// this.X = XmlUtil.AttrValue(node, "x", 0); -// this.Y = XmlUtil.AttrValue(node, "y", 0); -// this.Text = node.InnerText; -// this.GetTextStyle(svg); -// // check for tSpan tag -// if (node.InnerXml.IndexOf("<") >= 0) -// this.TextSpan = this.ParseTSpan(svg, node); -// 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 TextSpan2 TextSpan {get; private set;} - -// public override Fill Fill -// { -// get -// { -// Fill f = base.Fill; -// if (f == null) -// f = DefaultFill; -// return f; -// } -// } - -// public override Stroke Stroke -// { -// get -// { -// Stroke f = base.Stroke; -// if (f == null) -// f = DefaultStroke; -// return f; -// } -// } - -// TextSpan2 ParseTSpan(SVG svg, XmlNode node) -// { -// try -// { -// return TextSpan2.Parse(svg, node, this); -// } -// catch -// { -// return null; -// } -// } -// } - -// public class TextSpan2 : Shape -// { -// public enum eElementType -// { -// Tag, -// Text, -// } - -// 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 TextSpan2 End {get; set;} -// public List XList { get; set; } = new List(); -// public List YList { get; set; } = new List(); -// public List DXList { get; set; } = new List(); -// public List DYList { get; set; } = new List(); - -// public TextSpan2(SVG svg, Shape parent, string text) : base(svg, (XmlNode)null, parent) -// { -// this.ElementType = eElementType.Text; -// this.Text = text; -// } -// private List ParseNumberList(string value) -// { -// return value.Split(new[] { ' ', ',', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).Select(Double.Parse).ToList(); -// } -// 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(); -// if (!(attrs is null)) -// { -// foreach (var attr in attrs) -// { -// switch (attr.Name) -// { -// case "x": -// XList = ParseNumberList(attr.Value); -// break; -// case "y": -// YList = ParseNumberList(attr.Value); -// break; -// case "dx": -// DXList = ParseNumberList(attr.Value); -// break; -// case "dy": -// DYList = ParseNumberList(attr.Value); -// break; -// } -// } -// } -// } -// public override string ToString() -// { -// return this.Text; -// } - -// 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); -// if (end < 0) -// throw new Exception("Start '<' with no end '>'"); - -// end++; - -// string tagtext = text.Substring(start, end - start); -// if (tagtext.IndexOf("<", 1) > 0) -// throw new Exception(string.Format("Start '<' within tag 'tag'")); - -// List attrs = new List(); -// int attrstart = tagtext.IndexOf("tspan"); -// if (attrstart > 0) -// { -// attrstart += 5; -// while (attrstart < tagtext.Length-1) -// attrs.Add(StyleItem.ReadNextAttr(tagtext, ref attrstart)); -// } - -// 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) -// throw new Exception(string.Format("Start '<' within tag 'tag'")); - -// curPos = end; -// return tag; -// } - -// static TextSpan2 Parse(SVG svg, string text, ref int curPos, TextSpan2 parent, TextSpan2 curTag) -// { -// TextSpan2 tag = curTag; -// if (tag == null) -// tag = NextTag(svg, parent, text, ref curPos); -// while (curPos < text.Length) -// { -// int prevPos = 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 TextSpan2(svg, tag, s)); -// return tag; -// } -// if (next != null && next.StartIndex-prevPos > 0) -// { -// // pure text between tspan elements -// int diff = next.StartIndex-prevPos; -// string s = text.Substring(prevPos, diff); -// tag.Children.Add(new TextSpan2(svg, tag, s)); -// } -// if (next.Text.StartsWith("() -// }; - -// foreach (XmlNode child in node.ChildNodes) -// { -// ParseXmlNode(svg, rootSpan, child); -// } - -// return rootSpan; -// } - -// private static void ParseXmlNode(SVG svg, TextSpan2 parent, XmlNode node) -// { -// if (node.NodeType == XmlNodeType.Text || node.NodeType == XmlNodeType.CDATA) -// { -// parent.Children.Add(new TextSpan2(svg, parent, node.InnerText)); -// } -// else if (node.NodeType == XmlNodeType.Element && node.Name == "tspan") -// { -// var attrs = new List(); -// foreach (XmlAttribute attr in node.Attributes) -// { -// attrs.Add(new StyleItem(attr.Name, attr.Value)); -// } - -// var tspan = new TextSpan2(svg, parent, eElementType.Tag, attrs) -// { -// Text = node.OuterXml, -// StartIndex = 0 -// }; - -// foreach (XmlNode inner in node.ChildNodes) -// { -// ParseXmlNode(svg, tspan, inner); -// } - -// parent.Children.Add(tspan); -// } -// // optionally handle other text-related tags like here -// } - - -// public static void Print(TextSpan2 tag, string indent) -// { -// if (tag.ElementType == eElementType.Text) -// Console.WriteLine("{0} '{1}'", indent, tag.Text); -// indent += " "; -// foreach (TextSpan2 c in tag.Children) -// Print(c, indent); -// } -// } - -//} +using System; +using System.Collections.Generic; +using System.Xml; + +namespace SVGImage.SVG +{ + using Utils; + using Shapes; + using System.Linq; + + public sealed class TextShape2 : Shape + { + public TextShape2(SVG svg, XmlNode node, Shape parent) + : base(svg, node, parent) + { + this.X = XmlUtil.AttrValue(node, "x", 0); + this.Y = XmlUtil.AttrValue(node, "y", 0); + this.Text = node.InnerText; + this.GetTextStyle(svg); + // check for tSpan tag + if (node.InnerXml.IndexOf("<") >= 0) + this.TextSpan = this.ParseTSpan(svg, node.InnerXml); + } + + public double X { get; set; } + public double Y { get; set; } + public string Text { get; set; } + public TextSpan2 TextSpan { get; private set; } + + protected override Fill DefaultFill() + { + return Fill.CreateDefault(Svg, "black"); + } + protected override Stroke DefaultStroke() + { + return Stroke.CreateDefault(Svg, 0.1); + } + + TextSpan2 ParseTSpan(SVG svg, string tspanText) + { + try + { + return TextSpan2.Parse(svg, tspanText, this); + } + catch + { + return null; + } + } + } + public class TextSpan2 : Shape + { + public enum eElementType + { + Tag, + Text, + } + + 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 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 TextSpan2(SVG svg, Shape parent, eElementType eType, List attrs) + : base(svg, attrs, parent) + { + this.ElementType = eType; + this.Text = string.Empty; + this.Children = new List(); + } + public override string ToString() + { + return this.Text; + } + + 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); + if (end < 0) + throw new Exception("Start '<' with no end '>'"); + + end++; + + string tagtext = text.Substring(start, end - start); + if (tagtext.IndexOf("<", 1) > 0) + throw new Exception(string.Format("Start '<' within tag 'tag'")); + + List attrs = new List(); + int attrstart = tagtext.IndexOf("tspan"); + if (attrstart > 0) + { + attrstart += 5; + while (attrstart < tagtext.Length - 1) + attrs.Add(StyleItem.ReadNextAttr(tagtext, ref attrstart)); + } + + 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) + throw new Exception(string.Format("Start '<' within tag 'tag'")); + + curPos = end; + return tag; + } + + static TextSpan2 Parse(SVG svg, string text, ref int curPos, TextSpan2 parent, TextSpan2 curTag) + { + TextSpan2 tag = curTag; + if (tag == null) + tag = NextTag(svg, parent, text, ref curPos); + while (curPos < text.Length) + { + int prevPos = 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 TextSpan2(svg, tag, s)); + return tag; + } + if (next != null && next.StartIndex - prevPos > 0) + { + // pure text between tspan elements + int diff = next.StartIndex - prevPos; + string s = text.Substring(prevPos, diff); + 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..dfed7c6 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextPath.cs @@ -0,0 +1,17 @@ +using System; +using System.Xml; + +namespace SVGImage.SVG.Shapes +{ + 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..3f950ce --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextRender.cs @@ -0,0 +1,186 @@ +using System.Collections.Generic; +using System.Windows.Media; +using System.Windows; + +namespace SVGImage.SVG.Shapes +{ + using Utils; + using System.Linq; + using System.Windows.Markup; + + 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..f0a2ac9 --- /dev/null +++ b/Source/SVGImage/SVG/Shapes/TextString.cs @@ -0,0 +1,62 @@ +namespace SVGImage.SVG.Shapes +{ + using System.Linq; + using System.Text.RegularExpressions; + using System.Diagnostics; + + [DebuggerDisplay("{Text}")] + /// + /// Text not wrapped in a tspan element. + /// + public class TextString : ITextChild + { + public CharacterLayout[] Characters { get; set; } + public Shape Parent { get; set; } + public int Index { get; set; } + private static readonly Regex _trimmedWhitespace = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.Singleline); + 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/TextRender.cs deleted file mode 100644 index 06fc482..0000000 --- a/Source/SVGImage/SVG/TextRender.cs +++ /dev/null @@ -1,489 +0,0 @@ -//using System.Collections.Generic; -//using System.Windows.Media; -//using System.Windows; -//using System.Reflection; -//using System.Windows.Media.TextFormatting; - -//namespace SVGImage.SVG -//{ -// static class TextRender -// { -// private static int dpiX = 0; -// private static int dpiY = 0; - -// static public GeometryGroup BuildTextGeometry(TextShape shape) -// { -// double cursorX = 0; -// double cursorY = 0; -// return BuildTextSpan(shape.TextSpan, shape.TextStyle, ref cursorX, ref cursorY); -// //return BuildGlyphRun(shape, 0, 0); -// } - -// // Use GlyphRun to build the text. This allows us to define letter and word spacing -// // http://books.google.com/books?id=558i6t1dKEAC&pg=PA485&source=gbs_toc_r&cad=4#v=onepage&q&f=false -// static GeometryGroup BuildGlyphRun(TextShape shape) -// { -// 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, -// shape.TextSpan.XList, -// shape.TextSpan.YList, -// shape.TextSpan.DXList, -// shape.TextSpan.DYList)); -// return gp; -// } -// double cursorX = 0; -// double cursorY = 0; -// return BuildTextSpan(shape.TextSpan, shape.TextStyle, ref cursorX, ref cursorY); -// } - -// //static GeometryGroup BuildTextSpan(TextShape shape) -// //{ -// // double x = shape.X; -// // double y = shape.Y; -// // GeometryGroup gp = new GeometryGroup(); -// // BuildTextSpan(gp, shape.TextStyle, shape.TextSpan, ref x, ref y); -// // return gp; -// //} - -// private static GeometryGroup BuildTextSpan(TextSpan tspan, TextStyle style, ref double cursorX, ref double cursorY) -// { -// GeometryGroup group = new GeometryGroup(); -// double localX = cursorX; -// double localY = cursorY; - -// foreach (TextSpan child in tspan.Children) -// { -// double spanX = localX; -// double spanY = localY; - -// // Apply absolute positioning if x/y exist -// if (child.XList.Count > 0) -// { -// spanX = child.XList[0]; -// } -// else if (child.DXList.Count > 0) -// { -// spanX += child.DXList[0]; -// } - -// if (child.YList.Count > 0) -// { -// spanY = child.YList[0]; -// } -// else if (child.DYList.Count > 0) -// { -// spanY += child.DYList[0]; -// } - -// double totalWidth = 0.0; - -// if (child.ElementType == TextSpan.eElementType.Text) -// { -// Geometry gm = BuildGlyphRun( -// child.TextStyle ?? style, -// child.Text, -// spanX, -// spanY, -// ref totalWidth, -// child.XList, -// child.YList, -// child.DXList, -// child.DYList -// ); - -// group.Children.Add(gm); -// } -// else -// { -// Geometry gm = BuildTextSpan(child, child.TextStyle ?? style, ref spanX, ref spanY); -// group.Children.Add(gm); -// } - -// // Advance cursor only if this tspan didn't reset x -// if (child.XList.Count == 0) -// { -// localX = spanX + totalWidth; -// } -// else -// { -// localX = spanX; // reset after absolute x -// } - -// if (child.YList.Count == 0) -// { -// localY = spanY; -// } -// else -// { -// localY = spanY; -// } -// } - -// cursorX = localX; -// cursorY = localY; -// return group; -// } - -// 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) -// { -// //int xi = 0, yi = 0, dxi = 0, dyi = 0; -// //BuildTextSpan(gp, textStyle, tspan, ref x, ref y, ref xi, ref yi, ref dxi, ref dyi); -// var builtSpan = BuildTextSpan(tspan, textStyle, ref x, ref y); -// gp.Children.Add(builtSpan); - -// } - - - - - -// //static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle, -// // TextSpan tspan, ref double x, ref double y, -// // ref int xIndex, ref int yIndex, ref int dxIndex, ref int dyIndex) -// //{ -// // foreach (TextSpan child in tspan.Children) -// // { -// // double spanX = x; -// // double spanY = y; - -// // if (child.XList.Count > xIndex) { spanX = child.XList[xIndex++]; x = spanX; } -// // if (child.YList.Count > yIndex) { spanY = child.YList[yIndex++]; y = spanY; } - -// // if (child.DXList.Count > dxIndex) { spanX += child.DXList[dxIndex++]; x = spanX; } -// // if (child.DYList.Count > dyIndex) { spanY += child.DYList[dyIndex++]; y = spanY; } - -// // if (child.ElementType == TextSpan.eElementType.Text) -// // { -// // // Inherit position attributes from parent if not defined -// // List xList = (child.XList.Count > 0) ? child.XList : tspan.XList; -// // List yList = (child.YList.Count > 0) ? child.YList : tspan.YList; -// // List dxList = (child.DXList.Count > 0) ? child.DXList : tspan.DXList; -// // List dyList = (child.DYList.Count > 0) ? child.DYList : tspan.DYList; -// // string txt = child.Text; -// // double totalwidth = 0; -// // double baseline = y; - -// // if (child.TextStyle.BaseLineShift == "sub") -// // baseline += child.TextStyle.FontSize * 0.5; -// // if (child.TextStyle.BaseLineShift == "super") -// // baseline -= tspan.TextStyle.FontSize + (child.TextStyle.FontSize * 0.25); - -// // Geometry gm = BuildGlyphRun(child.TextStyle, txt, spanX, baseline, ref totalwidth, -// // xList, yList, dxList, dyList); -// // TextRender.SetElement(gm, child); -// // gp.Children.Add(gm); -// // x += totalwidth; -// // } -// // else if (child.ElementType == TextSpan.eElementType.Tag) -// // { -// // BuildTextSpan(gp, textStyle, child, ref x, ref y, ref xIndex, ref yIndex, ref dxIndex, ref dyIndex); -// // } -// // } -// //} - -// static Geometry BuildGlyphRun( -// TextStyle textStyle, -// string text, -// double x, -// double y, -// ref double totalWidth, -// List xList, -// List yList, -// List dxList, -// List dyList) -// { -// if (string.IsNullOrEmpty(text)) -// { -// return Geometry.Empty; -// } - -// 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; -// Typeface font = new Typeface(new FontFamily(textStyle.FontFamily), -// textStyle.Fontstyle, -// textStyle.Fontweight, -// FontStretch.FromOpenTypeStretch(9), -// new FontFamily("Arial Unicode MS")); -// GlyphTypeface glyphFace; - -// if (!font.TryGetGlyphTypeface(out glyphFace)) -// { -// return new GeometryGroup(); -// } - - -// List textChars = new List(); -// List glyphIndices = new List(); -// List advanceWidths = new List(); - -// for (int i = 0; i < text.Length; i++) -// { -// char textchar = text[i]; -// int codepoint = textchar; -// if (!glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out var glyphIndex)) -// { -// glyphIndices[i] = 0; // fallback to default glyph -// } -// glyphIndices.Add(glyphIndex); - -// double glyphWidth = glyphFace.AdvanceWidths[glyphIndex] * fontSize + textStyle.LetterSpacing; -// if (char.IsWhiteSpace(textchar)) -// glyphWidth += textStyle.WordSpacing; - -// textChars.Add(textchar); -// advanceWidths.Add(glyphWidth); -// } - -// Point baseline = new Point(x, y); -// List origins = new List(); -// double currentX = x; -// double currentY = y; - -// for (int i = 0; i < text.Length; i++) -// { -// if (xList != null && i < xList.Count) -// { -// currentX = xList[i]; -// } -// else if (dxList != null && i < dxList.Count) -// { -// currentX += dxList[i]; -// } - -// if (yList != null && i < yList.Count) -// { -// currentY = yList[i]; -// } -// else if (dyList != null && i < dyList.Count) -// { -// currentY += dyList[i]; -// } - -// origins.Add(new Point(currentX, -currentY)); -// currentX += advanceWidths[i]; -// } - -// totalWidth = currentX - x; - -// GlyphRun glyphs = new GlyphRun( -// glyphFace, -// 0, -// false, -// fontSize, -//#if !(DOTNET40 || DOTNET45 || DOTNET46) -// (float)(new DpiScale(dpiX, dpiY)).PixelsPerDip, -//#endif -// glyphIndices, -// new Point(baseline.X - 50, baseline.Y + 20), -// advanceWidths, -// origins, -// null, -// null, -// null, -// null, -// null -// ); - - - -// // add text decoration to geometry -// GeometryGroup gp = new GeometryGroup(); -// gp.Children.Add(glyphs.BuildGeometry()); -// if (textStyle.TextDecoration != null) -// { -// double decorationPos = 0; -// double decorationThickness = 0; -// if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Strikethrough) -// { -// decorationPos = baseline.Y - (font.StrikethroughPosition * fontSize); -// decorationThickness = font.StrikethroughThickness * fontSize; -// } -// if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Underline) -// { -// decorationPos = baseline.Y - (font.UnderlinePosition * fontSize); -// decorationThickness = font.UnderlineThickness * fontSize; -// } -// if (textStyle.TextDecoration[0].Location == TextDecorationLocation.OverLine) -// { -// decorationPos = baseline.Y - fontSize; -// decorationThickness = font.StrikethroughThickness * fontSize; -// } -// Rect bounds = new Rect(gp.Bounds.Left, decorationPos, gp.Bounds.Width, decorationThickness); -// gp.Children.Add(new RectangleGeometry(bounds)); - -// } -// return gp; -// } - - -// // static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double y, ref double totalwidth, -// // List xList = null, -// // List yList = null, -// // List dxList = null, -// // List dyList = null) -// // { -// // 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, -// // textStyle.Fontweight, -// // FontStretch.FromOpenTypeStretch(9), -// // new FontFamily("Arial Unicode MS")); -// // GlyphTypeface glyphFace; -// // double baseline = y; -// // if (!font.TryGetGlyphTypeface(out glyphFace)) -// // { -// // return new GeometryGroup(); -// // } -// //#if DOTNET40 || DOTNET45 || DOTNET46 -// // glyphs = new GlyphRun(); -// //#else -// // var dpiScale = new DpiScale(dpiX, dpiY); - -// // glyphs = new GlyphRun((float)dpiScale.PixelsPerDip); -// //#endif -// // ((System.ComponentModel.ISupportInitialize)glyphs).BeginInit(); -// // glyphs.GlyphTypeface = glyphFace; -// // glyphs.FontRenderingEmSize = fontSize; -// // List textChars = new List(); -// // List glyphIndices = new List(); -// // List advanceWidths = new List(); -// // totalwidth = 0; -// // char[] charsToSkip = new char[] { '\t', '\r', '\n' }; -// // List glyphOffsets = new List(); - -// // double currentX = x; -// // double currentY = y; -// // for (int i = 0; i < text.Length; ++i) -// // { -// // char textchar = text[i]; -// // int codepoint = textchar; - -// // if (!glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out ushort glyphIndex)) -// // continue; - -// // textChars.Add(textchar); - -// // double glyphWidth = glyphFace.AdvanceWidths[glyphIndex] * fontSize + textStyle.LetterSpacing; -// // if (char.IsWhiteSpace(textchar)) -// // glyphWidth += textStyle.WordSpacing; - -// // // Absolute overrides (apply once, not cumulative) -// // if (!(xList is null) && i < xList.Count) -// // currentX = xList[i]; -// // if (!(yList is null) && i < yList.Count) -// // currentY = yList[i]; - -// // // Relative offsets (cumulative) -// // if (!(dxList is null) && i < dxList.Count) -// // currentX += dxList[i]; -// // if (!(dyList is null) && i < dyList.Count) -// // currentY += dyList[i]; - -// // glyphIndices.Add(glyphIndex); -// // advanceWidths.Add(0); // Width will be zero since position is handled by offset -// // glyphOffsets.Add(new Point(currentX, -currentY)); - -// // currentX += glyphWidth; -// // totalwidth += glyphWidth; - - - -// // //char textchar = text[i]; -// // //int codepoint = textchar; -// // ////if (charsToSkip.Any(item => item == codepoint)) -// // //// continue; -// // //ushort glyphIndex; -// // //if (glyphFace.CharacterToGlyphMap.TryGetValue(codepoint, out glyphIndex) == false) -// // // 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]; -// // } -// // glyphs.GlyphOffsets = glyphOffsets.ToArray(); -// // //glyphs.GlyphOffsets = new Point[] { new Point(glyphOffsets[0].X, -glyphOffsets[0].Y), new Point(glyphOffsets[1].X, -glyphOffsets[1].Y), new Point(glyphOffsets[2].X, -glyphOffsets[2].Y) }; -// // glyphs.Characters = textChars.ToArray(); -// // glyphs.GlyphIndices = glyphIndices.ToArray(); -// // glyphs.AdvanceWidths = advanceWidths.ToArray(); - -// // // calculate text alignment -// // double alignmentoffset = 0; -// // if (textStyle.TextAlignment == TextAlignment.Center) -// // alignmentoffset = totalwidth / 2; -// // if (textStyle.TextAlignment == TextAlignment.Right) -// // alignmentoffset = totalwidth; - -// // baseline = y; -// // //glyphs.BaselineOrigin = new Point(x - alignmentoffset, baseline); -// // glyphs.BaselineOrigin = new Point(0 - alignmentoffset, 0); -// // ((System.ComponentModel.ISupportInitialize)glyphs).EndInit(); - - -// // // add text decoration to geometry -// // GeometryGroup gp = new GeometryGroup(); -// // gp.Children.Add(glyphs.BuildGeometry()); -// // if (textStyle.TextDecoration != null) -// // { -// // double decorationPos = 0; -// // double decorationThickness = 0; -// // if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Strikethrough) -// // { -// // decorationPos = baseline - (font.StrikethroughPosition * fontSize); -// // decorationThickness = font.StrikethroughThickness * fontSize; -// // } -// // if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Underline) -// // { -// // decorationPos = baseline - (font.UnderlinePosition * fontSize); -// // decorationThickness = font.UnderlineThickness * fontSize; -// // } -// // if (textStyle.TextDecoration[0].Location == TextDecorationLocation.OverLine) -// // { -// // decorationPos = baseline - fontSize; -// // decorationThickness = font.StrikethroughThickness * fontSize; -// // } -// // Rect bounds = new Rect(gp.Bounds.Left, decorationPos, gp.Bounds.Width, decorationThickness); -// // gp.Children.Add(new RectangleGeometry(bounds)); - -// // } -// // return gp; -// // } -// } -//} diff --git a/Source/SVGImage/SVG/TextRender2.cs b/Source/SVGImage/SVG/TextRender2.cs new file mode 100644 index 0000000..b9289d9 --- /dev/null +++ b/Source/SVGImage/SVG/TextRender2.cs @@ -0,0 +1,158 @@ +using System.Collections.Generic; +using System.Windows.Media; +using System.Windows; +using System.Reflection; +using SVGImage.SVG.Shapes; +using System.Linq; +using SVGImage.SVG.Utils; + +namespace SVGImage.SVG +{ + public class TextRender2 : TextRenderBase + { + public override GeometryGroup BuildTextGeometry(TextShape shape) + { + return BuildGlyphRun(shape, 0, 0); + } + + // Use GlyphRun to build the text. This allows us to define letter and word spacing + // http://books.google.com/books?id=558i6t1dKEAC&pg=PA485&source=gbs_toc_r&cad=4#v=onepage&q&f=false + static GeometryGroup BuildGlyphRun(TextShape shape, double xoffset, double yoffset) + { + GeometryGroup gp = new GeometryGroup(); + return BuildTextSpan(shape); + } + + static GeometryGroup BuildTextSpan(TextShape shape) + { + double x = shape.X.FirstOrDefault().Value; + double y = shape.Y.FirstOrDefault().Value; + GeometryGroup gp = new GeometryGroup(); + BuildTextSpan(gp, shape.TextStyle, shape, ref x, ref y); + return gp; + } + + + + static void BuildTextSpan(GeometryGroup gp, TextStyle textStyle, + TextShapeBase tspan, ref double x, ref double y) + { + foreach (ITextNode child in tspan.Children) + { + if (child is TextString textString) + { + string txt = textString.Text; + double totalwidth = 0; + double baseline = y; + + 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(textString.TextStyle, txt, x, baseline, ref totalwidth); + TextRender2.SetElement(gm, textString); + gp.Children.Add(gm); + x += totalwidth; + } + else if (child is TextShapeBase childTspan) + { + BuildTextSpan(gp, textStyle, childTspan, ref x, ref y); + } + } + } + + static Geometry BuildGlyphRun(TextStyle textStyle, string text, double x, double y, ref double totalwidth) + { + if (string.IsNullOrEmpty(text)) + return new GeometryGroup(); + + double fontSize = textStyle.FontSize; + GlyphRun glyphs = null; + Typeface font = new Typeface(new FontFamily(textStyle.FontFamily), + textStyle.Fontstyle, + textStyle.Fontweight, + FontStretch.FromOpenTypeStretch(9), + new FontFamily("Arial Unicode MS")); + GlyphTypeface glyphFace; + double baseline = y; + if (font.TryGetGlyphTypeface(out glyphFace)) + { +#if DPI_AWARE + glyphs = new GlyphRun((float)DpiUtil.PixelsPerDip); +#else + glyphs = new GlyphRun(); +#endif + ((System.ComponentModel.ISupportInitialize)glyphs).BeginInit(); + glyphs.GlyphTypeface = glyphFace; + glyphs.FontRenderingEmSize = fontSize; + List textChars = new List(); + List glyphIndices = new List(); + List advanceWidths = new List(); + totalwidth = 0; + char[] charsToSkip = new char[] { '\t', '\r', '\n' }; + for (int i = 0; i < text.Length; ++i) + { + char textchar = text[i]; + int codepoint = textchar; + //if (charsToSkip.Any(item => item == codepoint)) + // continue; + ushort glyphIndex; + 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]; + } + glyphs.Characters = textChars.ToArray(); + glyphs.GlyphIndices = glyphIndices.ToArray(); + glyphs.AdvanceWidths = advanceWidths.ToArray(); + + // calculate text alignment + double alignmentoffset = 0; + if (textStyle.TextAlignment == TextAlignment.Center) + alignmentoffset = totalwidth / 2; + if (textStyle.TextAlignment == TextAlignment.Right) + alignmentoffset = totalwidth; + + baseline = y; + glyphs.BaselineOrigin = new Point(x - alignmentoffset, baseline); + ((System.ComponentModel.ISupportInitialize)glyphs).EndInit(); + } + else + return new GeometryGroup(); + + // add text decoration to geometry + GeometryGroup gp = new GeometryGroup(); + gp.Children.Add(glyphs.BuildGeometry()); + if (textStyle.TextDecoration != null) + { + double decorationPos = 0; + double decorationThickness = 0; + if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Strikethrough) + { + decorationPos = baseline - (font.StrikethroughPosition * fontSize); + decorationThickness = font.StrikethroughThickness * fontSize; + } + if (textStyle.TextDecoration[0].Location == TextDecorationLocation.Underline) + { + decorationPos = baseline - (font.UnderlinePosition * fontSize); + decorationThickness = font.UnderlineThickness * fontSize; + } + if (textStyle.TextDecoration[0].Location == TextDecorationLocation.OverLine) + { + decorationPos = baseline - fontSize; + decorationThickness = font.StrikethroughThickness * fontSize; + } + Rect bounds = new Rect(gp.Bounds.Left, decorationPos, gp.Bounds.Width, decorationThickness); + gp.Children.Add(new RectangleGeometry(bounds)); + + } + return gp; + } + } +} \ No newline at end of file diff --git a/Source/SVGImage/SVG/TextStyle.cs b/Source/SVGImage/SVG/TextStyle.cs index 52022c9..f343e14 100644 --- a/Source/SVGImage/SVG/TextStyle.cs +++ b/Source/SVGImage/SVG/TextStyle.cs @@ -2,11 +2,17 @@ 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() { @@ -48,7 +54,8 @@ public TextStyle(Shape owner) public Typeface GetTypeface() { - return new Typeface(new FontFamily(FontFamily), + var fontFamily = _fontResolver.ResolveFontFamily(FontFamily); + return new Typeface(fontFamily, Fontstyle, Fontweight, FontStretch.FromOpenTypeStretch(9), @@ -70,7 +77,7 @@ public GlyphTypeface GetGlyphTypeface() 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 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; } @@ -101,9 +108,47 @@ public static TextStyle Merge(TextStyle baseStyle, TextStyle overrides) 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(); + } + } + + + + +} From e4781c3d76405b804b87c25fabb2b1e83fb8d1e2 Mon Sep 17 00:00:00 2001 From: Mike Wagner Date: Mon, 28 Jul 2025 18:31:43 -0400 Subject: [PATCH 4/5] Documentation --- .../SVG/Shapes/LengthPercentageOrNumber.cs | 27 ++++++++++++++++--- .../Shapes/LengthPercentageOrNumberList.cs | 12 +++++++++ Source/SVGImage/SVG/Shapes/Text.cs | 3 +++ Source/SVGImage/SVG/Shapes/TextPath.cs | 3 +++ Source/SVGImage/SVG/Shapes/TextRender.cs | 5 ++-- Source/SVGImage/SVG/Shapes/TextString.cs | 12 ++++++--- 6 files changed, 54 insertions(+), 8 deletions(-) diff --git a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs index 0ae8898..4b9ca9b 100644 --- a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs +++ b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumber.cs @@ -9,6 +9,9 @@ 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(); @@ -130,15 +133,33 @@ private double ResolveValue() } } /// - /// + /// Represents a length that may have a value that is context dependent. /// - /// - /// If null, units will be ignored + /// 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()); diff --git a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs index 3ed27e8..0bddebf 100644 --- a/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs +++ b/Source/SVGImage/SVG/Shapes/LengthPercentageOrNumberList.cs @@ -18,6 +18,18 @@ private LengthPercentageOrNumberList(Shape owner, LengthOrientation orientation _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); diff --git a/Source/SVGImage/SVG/Shapes/Text.cs b/Source/SVGImage/SVG/Shapes/Text.cs index e51344e..4ba2e0a 100644 --- a/Source/SVGImage/SVG/Shapes/Text.cs +++ b/Source/SVGImage/SVG/Shapes/Text.cs @@ -8,6 +8,9 @@ namespace SVGImage.SVG using Shapes; using System.Linq; + /// + /// The original TextShape class. + /// public sealed class TextShape2 : Shape { public TextShape2(SVG svg, XmlNode node, Shape parent) diff --git a/Source/SVGImage/SVG/Shapes/TextPath.cs b/Source/SVGImage/SVG/Shapes/TextPath.cs index dfed7c6..e31749f 100644 --- a/Source/SVGImage/SVG/Shapes/TextPath.cs +++ b/Source/SVGImage/SVG/Shapes/TextPath.cs @@ -3,6 +3,9 @@ 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) diff --git a/Source/SVGImage/SVG/Shapes/TextRender.cs b/Source/SVGImage/SVG/Shapes/TextRender.cs index 3f950ce..cdd1a12 100644 --- a/Source/SVGImage/SVG/Shapes/TextRender.cs +++ b/Source/SVGImage/SVG/Shapes/TextRender.cs @@ -8,10 +8,11 @@ namespace SVGImage.SVG.Shapes 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) { diff --git a/Source/SVGImage/SVG/Shapes/TextString.cs b/Source/SVGImage/SVG/Shapes/TextString.cs index f0a2ac9..7b0af19 100644 --- a/Source/SVGImage/SVG/Shapes/TextString.cs +++ b/Source/SVGImage/SVG/Shapes/TextString.cs @@ -6,14 +6,20 @@ [DebuggerDisplay("{Text}")] /// - /// Text not wrapped in a tspan element. + /// 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; } - public int Index { get; set; } - private static readonly Regex _trimmedWhitespace = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.Singleline); + /// + /// 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; From df22b6f23c5ce91fb1adccbb6a4ec0bc94d0f60e Mon Sep 17 00:00:00 2001 From: Mike Wagner Date: Mon, 28 Jul 2025 18:38:27 -0400 Subject: [PATCH 5/5] Reenabled PackageValidation --- Source/SVGImage/DotNetProjects.SVGImage.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/SVGImage/DotNetProjects.SVGImage.csproj b/Source/SVGImage/DotNetProjects.SVGImage.csproj index e704de2..02886db 100644 --- a/Source/SVGImage/DotNetProjects.SVGImage.csproj +++ b/Source/SVGImage/DotNetProjects.SVGImage.csproj @@ -29,7 +29,7 @@ images\dotnetprojects.png svg wpf svg-icons svg-to-png svg-to-xaml svgimage svgimage-control Readme.md - false + true 5.1.0